@getplate/core
v7.2.1
Published
This package contains a variety of useful implementations for NestJS services.
Downloads
19
Readme
@getplate/core
This package contains the core functionality of the Plate NestJS microservices.
Getting Started
To add this to your project: yarn add @getplate/core
or npm install @getplate/core
.
Features
class-transformer
TransformDate
decorator, to automatically transform dates to ISO strings and back.
class-validator
ClassValidatorError
for throwing exceptions (class-validator errors are not actually error classes, so this wrapper makes them errors).AtLeastOneDefined
decorator, to validate that at least one of the given properties is defined.ExactlyOneDefined
decorator, to validate that exactly one of the given properties is defined.IsFileName
decorator, to validate that the given string is a valid file name. Also exported as a regular method.GreaterThan
decorator, to validate that the given number is greater than another property.LessThan
decorator, to validate that the given number is less than another property.GreaterThanOrEqual
decorator, to validate that the given number is greater than or equal to another property.LessThanOrEqual
decorator, to validate that the given number is less than or equal to another property.
Configuration
A global NestJS module which loads the environment variables into a configuration object and validates it using class-validator.
Getting started
To use this module, import it into your root module and call ConfigModule#forRoot
in the imports array. The Config
class will be available globally.
Environment variables must have the same case as the class properties.
A good rule of thumb is to use replace nested class fields with double underscores (e.g. config.foo.bar
becomes foo__bar
).
Example:
// Environment variables
NODE_ENV = dev
foo__bar = bar
// foo.config.ts
export class FooConfig {
@IsDefined()
public readonly bar!: string;
}
// config.ts
export class Config {
@IsDefined()
@IsIn(["dev", "test", "production"])
public readonly nodeEnv!: AavailableNodeEnvs;
@IsDefined()
@Type(() => NestedConfig)
@ValidateNested()
public readonly foo!: FooConfig;
}
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
schema: Config,
}),
],
})
export class AppModule {
}
Changing the configuration
There are a few things to note when changing anything regarding the configuration:
- Make sure to put any environment variables required to run the service in the
.env.sample
file, so others can know which environment variables to set up. - To parse nested objects from environment variables, use the separator specified in
app.module.ts
. Example using separator__
(double underscore):db__url
turns intoconfig.db.url
- Nested objects must be defined in the environment variables. Otherwise, the object will not be parsed and the parent
config will return
undefined
. If there are multiple fields in the nested object, only one needs to be defined. The other fields will take the default values given in the config object. - The format of the environment variables matters. Keeping them
camelCase
removes the need for normalization. - Dashes/underscores are not parsed, so they must be normalized (like
NODE_ENV
, which is normalized by default). - When normalizing optional fields, make sure to check whether the non-normalized version is undefined to avoid overriding the default value with undefined:
if (config.SOME_OPTIONAL_VAR_WHICH_NEEDS_NORMALIZING !== undefined) {
config.someOptionalVarWhichNeedsNormalizing =
config.SOME_OPTIONAL_VAR_WHICH_NEEDS_NORMALIZING;
}
Authentication/Authorization
- Global
AuthorizationModule
for initializing the module with default options. - Globally enables the
AuthorizationGuard
. Automatically recognizes CRUD methods and applies the correct permissions. Otherwise, you must manually set the action. For now, allows all microservice messages. - Provides a
AuthorizationOptions
decorator to modify the authorization options for a resolver or route. UnauthorizedError
for when users try to access a protected resource without sending a proper Authorization header.ForbiddenError
for when users try to access a resource they are not authorized to access.SubjectMiddleware
to add the subject (automatically detects users or Api Tokens) to the request scope. Register it like this:// app.module.ts @Module({ // Module config }) export class AppModule implements NestModule { public configure(consumer: MiddlewareConsumer): void { consumer .apply(SubjectMiddleware) .forRoutes({ path: "/graphql", method: RequestMethod.POST }); } }
CurrentSubject
decorator to get the subject from the request cross-context. It has the typeKratosIdentityTokenPayload
orApiTokenPayload
(automatically detected based on Authorization header).
GraphQL
- Global
GraphQLModule
for initializing Nest’s GraphQL module with default options (including Apollo Federation 2). - Array arguments (Nest’s default
ValidationPipe
does not work for arrays, and theParseArrayPipe
only works for REST)IArrayArgsType<TInput>
interface to use in place of yourTInput[]
argument.ArrayArgsType<TInput>
function to extend your argument class from (see documentation comment for more info).
InfoData
interface to use as the type of theinfo
argument in your resolvers.
Ordering
OrderDirection
enum to determine ascending or descending ordering.OrderOptionsInput
input to use as argument in your resolver.
Example:
@Resolver()
export class SomeResolver {
@Query()
public async findAll(
@Args("orderBy", {
nullable: true,
})
orderOptions: OrderOptionsInput
): Promise<SomeModelConnection> {
// Your logic here
}
}
Filtering
FilterField
decorator to mark fields as filterable. Use@FilterField({ isRelation: true })
for relations.PRN_FILTER_OPTIONS
object to use in@FilterField
forPRN
fields.Filter
function which returns a generated filter input class.PrimitiveFilter
union, combining all primitive filter classes (also exported individually).
Example:
@ObjectType()
export class User {
@Field()
@FilterField()
public id!: string;
@Field()
@FilterField()
public name!: string;
@Field(() => [User])
@FilterField({isRelation: true})
public friends!: User[];
}
export const UsersFilterInput = Filter(User);
Pagination
Cursor
for storing and generating the data about cursors.PageInfo
payload class.Paginated
function to inherit from to create the paginated payload (see the example in the documentation comment).Connection
interface to serve as the root type of a paginated response (includes anEdge
interface to define edges).PaginationOptionsArgs
input class for receiving pagination arguments.
Example:
export class User {
@Field()
public id!: string;
@Field()
public name!: string;
}
@ObjectType()
export class UsersConnection extends Paginated(User) {
}
findAll
resolver example
@ObjectType()
export class User {
@Field()
@FilterField()
public id!: string;
@Field()
@FilterField()
public name!: string;
@Field(() => [User])
@FilterField({isRelation: true})
public friends!: User[];
}
export const UsersFilterInput = Filter(User);
@ObjectType()
export class UsersConnection extends Paginated(User) {
}
@ArgsType()
export class FindAllUsersArgs implements FindAllArgs<User> {
@Field(() => PaginationOptionsInput)
@IsDefined()
@ValidateNested()
public readonly paginate!: PaginationOptionsInput;
@Field(() => OrderOptionsInput, {
nullable: true,
defaultValue: {},
})
@IsDefined()
@ValidateNested()
public readonly orderBy!: OrderOptionsInput;
@Field(() => [UsersFilterInput], {nullable: true})
@IsOptional()
@Type(() => UsersFilterInput)
@ValidateNested({each: true})
public readonly where?: FilterInput<User>[];
}
@Resolver(() => User)
export class UsersResolver {
@Query(() => UsersConnection, {
name: "users",
description: `Orders and paginates all Users.`,
})
public override async findAll(
@Args() {paginate, orderBy, where}: FindAllUsersArgs
): Promise<Connection<Users>> {
// Implementation...
}
}
PRN helpers
PrnScalar
so the consuming service can just usePRN
classes asField
types (this is already registered in theGraphQLModule
, so you do not need to register it yourself)PrnField
to mark an id field as aPRN
. See the decorator’s description for more information. WARNING: This requires you to generate thePRN
field using@ResolveField
in the model’s resolver. Example:// some.model.ts @ObjectType() export class SomeModel { @PrnField() // Automatically converts `id` to `prn` public id!: string; } // some.resolver.ts @Resolver(() => SomeModel) export class SomeResolver { @ResolveField(() => PRN) public prn(@Parent() entity: SomeModel, @Context() context: any): string { return new PRN( this.config.partition, entity.organizationId, ServiceAbbreviation.YOUR_SERVICE, "some-model", entity.id ).toString(); } }
PrnArgs
to use an id parameter in a resolver. See the decorator’s description for more information.
Federation
We use Apollo Federation 2 for our GraphQL subgraphs. See the NestJS documentation for the basics.
Reference
interface to use as return type in@ResolveField
method for federated types and in@ResolveReference
when resolving a federated type (see example below). This contains the__typename
and theprn
field. For composite keys, inherit from the baseReference
class and add the additional fields.
Example:
// federated.model.ts
@ObjectType()
@Directive(`key(fields: "prn compositeId")`)
export class FederatedModel {
@PrnField()
public id!: string;
@Field()
public compositeId!: string;
}
// federated.resolver.ts
@Resolver(() => FederatedModel)
export class FederatedModelResolver {
@ResolveReference()
public async resolveReference(reference: Reference): Promise<Asset> {
// Your fetching logic here
}
}
// other.model.ts
@ObjectType()
export class OtherModel {
@PrnField()
public id!: string;
@Field(() => FederatedModel)
public federatedModel!: FederatedModel;
}
// other.resolver.ts
@Resolver(() => OtherModel)
export class OtherModelResolver {
@ResolveField(() => FederatedModel)
public federatedModel(@Parent() entity: OtherModel): Reference {
return {
__typename: FederatedModel.name,
prn: entity.federatedModel.id,
compositeId: entity.federatedModel.compositeId,
};
}
}
For composite keys, it is required to store all identifying fields in the Reference
, key
directive, and in the
database.
Microservices
Global
MicroservicesWrapperModule
to initialize the wrapper. This takes in a serverQueueId
and optionally a list of clientQueueId
s.MicroservicesProxy
(provider) to send messages to other services. Inject this to send messages.SubscribeToEvent
,SubscribeToMessage
, andSubscribeToGlobalEvent
decorators to add message handlers.Strongly typed messages. To add your own strongly typed message, add the following to this project:
// messages-data.dto.ts export interface YourMessageData { // Your data } // message-handlers.interface.ts export interface MessageHandlers { // Other handlers onYourMessage?(data: YourMessageData): Promise<YourMessageResponse>; } // microservice-identifiers.enum.ts export const enum MessageId { // Other messages YOUR_MESSAGE = "YOUR_MESSAGE", } // microservices-proxy.ts export class MicroservicesProxy { // Other methods public async sendYourMessage( data: YourMessageData ): Promise<YourMessageResponse> { return this.sendMessage(QueueId.YOUR_QUEUE, MessageId.YOUR_MESSAGE, data); // Or sendEvent, sendGlobalEvent } }
subscribe to the message in a controller in your microservice consumer like this:
// some.controller.ts @Controller() export class SomeController implements MessageHandlers { @SubscribeToMessage(MessageId.YOUR_MESSAGE) public async onYourMessage( data: YourMessageData ): Promise<YourMessageResponse> { // Handle message } }
send the message from a microservice publisher like this:
// some.service.ts @Injectable() export class SomeService { public constructor( private readonly microservicesProxy: MicroservicesProxy ) {} public async someMethod(): Promise<void> { const response = await this.microservicesProxy.sendYourMessage({ // Your data }); } }
QueueId
,MessageId
enums for interacting with queues and messages.MessageHandlers
interface containing signatures to use when consuming messages (see documentation comment for more information).DTO classes for the data to pass in a message.
Errors
NotFoundError
for when a resource is requested which does not exist.InvalidArgumentError
for when users try to call a resolver or route without the correct arguments.BadRequestError
for when users try to call an endpoint with data which is invalid (theInvalidArgumentError
is for invalid arguments directly, whileBadRequestError
is a more high-level constraint being unfulfilled).UnprocessableEntityException
for when users try to call an endpoint with data which is invalid ( theInvalidArgumentError
is for invalid arguments directly, whileUnprocessableEntityException
is a more high-level constraint being unfulfilled).
Headers
Headers
decorator to get one or more headers from the request cross-context.RequiredHeader
decorator to get a header from the request cross-context and validates it. If invalid, throws anUnprocessableEntityException
.PrnHeader
decorator to get a header from the request cross-context and validates it as aPRN
. If invalid, throws anUnprocessableEntityException
.EnumHeader
decorator to get a header from the request cross-context and validates it as an enum. If invalid, throws anUnprocessableEntityException
.
Other
RequestScope
interface for passing data across the whole request.AvailableNodeEnvs
enum andisNodeEnv
method to make interacting withNODE_ENV
easier.SLUG_REGEX
string
andRegExp
object to verify the format of slugs.generateMermaidDependencyGraph
function to generate a mermaid dependency graph from a list of classes.- Install the package
yarn install --dev nestjs-spelunker
- Add
const mermaidGraph = generateMermaidDependencyGraph(app);
to the end of yourmain.ts#bootstrap
function. - Copy and paste the result of this function into https://mermaid.live or https://draw.io (or any other mermaid graph visualizer).
- Install the package
ApiTokenData
interface which defines the data that is stored in an Api Token.initTracing
function to initialize tracing for the service.