diff --git a/packages/hasura/README.md b/packages/hasura/README.md index 6a408aa93..9e0109a9b 100644 --- a/packages/hasura/README.md +++ b/packages/hasura/README.md @@ -8,15 +8,37 @@ Leverage NestJS to make incorporating business logic and event processing easier license

+- [@golevelup/nestjs-hasura](#golevelupnestjs-hasura) + - [Features](#features) + - [Usage](#usage) + - [Install](#install) + - [Import](#import) + - [Configuration](#configuration) + - [Usage](#usage-1) + - [Integrating with your NestJS app](#integrating-with-your-nestjs-app) + - [Automatically Synchronize Hasura Metadata](#automatically-synchronize-hasura-metadata) + - [Opting Out](#opting-out) + - [Registering Table Event Handlers](#registering-table-event-handlers) + - [Registering Scheduled Event Handlers](#registering-scheduled-event-handlers) + - [Retry Configuration](#retry-configuration) + - [Configuring Hasura Environment Variables](#configuring-hasura-environment-variables) + - [Related Hasura Documentation](#related-hasura-documentation) + - [Concepts](#concepts) + - [Tutorials](#tutorials) + - [Contribute](#contribute) + - [License](#license) + ## Features - 🎉 Exposes an API endpoint from your NestJS application at to be used for event processing from Hasura. Defaults to `/hasura/events/` but can be easily configured - 🔒 Automatically validates that the event payload was actually sent from Hasura using configurable secrets -- 🕵️ Discovers providers from your application decorated with `HasuraEventHandler` and routes incoming events to them +- 🕵️ Discovers methods from your application and automatically turns them into Hasura event handlers. Supports insert, update and delete events from your tables as well as scheduled events based on a CRON schedule + +- 🧭 Routes incoming webhook payloads to the correct event handler based on configuration so you can maintain a single webhook endpoint for Hasura -- 🧭 Leverage the table and schema name of the event to route to the appropriate handler +- 🔌 Optionally supports automatic management of your Hasura metadata files which means that your application code can be the source of truth for configuration of events. This reduces a ton of boilerplate and developer overhead ## Usage @@ -32,15 +54,43 @@ or Import and add `HasuraModule` to the `imports` section of the consuming module (most likely `AppModule`). In order to ensure that your Hasura events webhook endpoint is secure, the module requires configuration for an HTTP header name and value that will be used to verify that the event actually came from Hasura. +### Configuration + +The Hasura Module supports both the `forRoot` and `forRootAsync` patterns for configuration, so you can easily retrieve the necessary config values from a `ConfigService` or other provider. + +### Usage + +#### Integrating with your NestJS app + +The `HasuraModule` makes it easy to reuse the same events API endpoint for all events that you create in Hasura. The internal routing mechanism on the NestJS side ensures that the all events coming in through the endpoint will be sent to the correct handler. The endpoint provided defaults to `/hasura/events`. This can be overriden with the module by specifying an alternative `controllerPrefix` so for example you could set this to `webhooks` and the resulting endpoint would be available at `/webhooks/events`. + +#### Automatically Synchronize Hasura Metadata + +One of the more powerful features of this Module is the ability to automatically generate the necessary Hasura metadata for your event handlers instead of having to worry about configuring each handler individually. Under the hood, this uses the [`@hasura/metadata`](https://www.npmjs.com/package/@hasura/metadata) to generate and merge changes to your `tables.yaml` and `cron_triggers.yaml` files. + +If you decide to opt into this functionality, you should include the optional `managedMetaDataConfig` object when importing the HasuraModule into your application. + ```typescript import { HasuraModule } from '@golevelup/nestjs-hasura'; @Module({ imports: [ HasuraModule.forRoot(HasuraModule, { - secretFactory: secret, - secretHeader: secretHeader, - controllerPrefix: 'something', // this is optional. defaults to hasura + webhookConfig: { + secretFactory: secret, + secretHeader: secretHeader, + }, + managedMetaDataConfig: { + dirPath: join(process.cwd(), 'hasura/metadata'), + secretHeaderEnvName: 'HASURA_NESTJS_WEBHOOK_SECRET_HEADER_VALUE', + nestEndpointEnvName: 'NESTJS_EVENT_WEBHOOK_ENDPOINT', + defaultEventRetryConfig: { + intervalInSeconds: 15, + numRetries: 3, + timeoutInSeconds: 100, + toleranceSeconds: 21600, + }, + }, }), ], }) @@ -49,55 +99,78 @@ export class AppModule { } ``` -### Configuration +It is recommended that you conditionally add this configuration based on the Node Environment as this should only be used in development environments to track the necessary changes to your metadata yaml files so that they can be tracked in source control. -The Hasura Module supports both the `forRoot` and `forRootAsync` patterns for configuration, so you can easily retrieve the necessary config values from a `ConfigService` or other provider. +After generating changes to these files you should make sure they are applied against your Hasura instance using the CLI command: -### Registering Event Handlers +``` +hasura metadata apply +``` -Decorate methods in your NestJS providers in order to have them be automatically attached as event handlers for incoming Hasura events. The event payload will be analyzed and routed to your provider methods based on the configuration provided in the decorator. +#### Opting Out -#### Route based on Hasura Trigger Name +If you decide to opt out of automatic metadata synchronization it is up to you to ensure that the secret header name and values match. When creating the event in the Hasura console, you should set these values such that they match the configuration provided to the `HasuraModule` configuration in your NestJS application. This ensures that only Hasura can trigger events in your system. -The recommended method of routing to the correct event handler is to specify the Hasura Trigger Name in the decorator. This will ensure that you have the flexibility to have multiple events targeting the same table with different operation types and column sets. +#### Registering Table Event Handlers + +Decorate methods in your NestJS providers in order to have them be automatically attached as event handlers for incoming Hasura events. The event payload will be analyzed and routed to your provider methods based on the configuration provided in the decorator. ```typescript -import { HasuraEventHandler, HasuraEvent } from '@golevelup/nestjs-hasura'; +import { + TrackedHasuraEventHandler, + HasuraUpdateEvent, + HasuraInsertEvent, +} from '@golevelup/nestjs-hasura'; @Injectable() class UsersService { - @HasuraEventHandler({ - triggerName: 'user_created', + @TrackedHasuraEventHandler({ + triggerName: 'user-created', + tableName: 'user', + definition: { type: 'insert' }, }) - handleUserCreated(evt: HasuraEvent) { - // handle the event payload. Typing the method parameter with `HasurEvent` will provide intellisense - } + handleUserCreated(evt: HasuraInsertEvent) {} + + @TrackedHasuraEventHandler({ + triggerName: 'user-updated', + tableName: 'user', + definition: { type: 'update', columns: ['avatarUrl'] }, + }) + handleUserUpdated(evt: HasuraUpdateEvent) {} } ``` -#### Route Based on Schema and Table Name (Deprecated) - -It is possible to configure routing to the event handler based on the schema and table name of the source event. This is deprecated and not recommended as it is a less flexible way to route events and will be removed in a future release. - -The schema name is optional and if not provided will default to `public`. - -For example, to create an event handler for events coming from a `users` table in your database in the `public` schema a method handler could be decorated: +#### Registering Scheduled Event Handlers ```typescript -import { HasuraEventHandler, HasuraEvent } from '@golevelup/nestjs-hasura'; +import { TrackedHasuraScheduledEventHandler } from '@golevelup/nestjs-hasura'; @Injectable() -class UsersService { - @HasuraEventHandler({ - table: { name: 'user' }, +class RecurringJobService { + @TrackedHasuraScheduledEventHandler({ + cronSchedule: CommonCronSchedules.EveryMinute, + name: 'every-minute', + payload: {}, + comment: 'this is my comment', }) - handleUserCreated(evt: HasuraEvent) { - // handle the event payload. Typing the method parameter with `HasurEvent` will provide intellisense + public async cronTask(evt: any) { + this.logger.log(evt); } } ``` -### Creating Hasura Events +#### Retry Configuration + +Retry configuration for both Table Event handlers as well as Scheduled Event handlers can be configured on the individual decorator or you can provide a default retry configuration at the module level that will be used for any event handler that does not explicitly provide its own retry settings. + +#### Configuring Hasura Environment Variables + +You should provide ENV variables to your Hasura instance that map the webhook endpoint and secret header values for communication to your NestJS application. + +In the examples above, `HASURA_NESTJS_WEBHOOK_SECRET_HEADER_VALUE` and `NESTJS_EVENT_WEBHOOK_ENDPOINT` were used. The webhook endpoint should point to the automatically scaffolded events endpoint eg: +`https://my-nest-app.com/api/hasura/events` + +### Related Hasura Documentation #### Concepts @@ -108,14 +181,6 @@ https://hasura.io/docs/1.0/graphql/manual/event-triggers/index.html#event-trigge https://hasura.io/docs/1.0/graphql/manual/getting-started/first-event-trigger.html https://hasura.io/event-triggers -#### Integrating with your NestJS app - -The `HasuraModule` makes it easy to reuse the same events API endpoint for all events that you create in Hasura. The internal routing mechanism on the NestJS side ensures that the all events coming in through the endpoint will be sent to the correct handler. The endpoint provided defaults to `/hasura/events`. This can be overriden with the module by specifying an alternative `controllerPrefix` so for example you could set this to `webhooks` and the resulting endpoint would be available at `/webhooks/events`. - -#### Important! - -when creating the event in Hasura, ensure that the Header Name and Value match the configuration provided to the `HasuraModule` configuration in your NestJS application. This ensures that only Hasura can trigger events in your system. - ## Contribute Contributions welcome! Read the [contribution guidelines](../../CONTRIBUTING.md) first. diff --git a/packages/hasura/package.json b/packages/hasura/package.json index dab161aa0..fc79846f6 100644 --- a/packages/hasura/package.json +++ b/packages/hasura/package.json @@ -38,7 +38,9 @@ "dependencies": { "@golevelup/nestjs-common": "^1.4.2", "@golevelup/nestjs-discovery": "^2.3.1", - "@golevelup/nestjs-modules": "^0.4.1" + "@golevelup/nestjs-modules": "^0.4.1", + "@hasura/metadata": "^1.0.2", + "js-yaml": "^3.14.1" }, "jest": { "moduleFileExtensions": [ @@ -54,5 +56,8 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" }, - "gitHead": "6f97aab8ce9d65dc074750a3ee467ec5ff3b9908" + "gitHead": "6f97aab8ce9d65dc074750a3ee467ec5ff3b9908", + "devDependencies": { + "@types/js-yaml": "^3.12.5" + } } diff --git a/packages/hasura/src/hasura-metadata-dist/HasuraMetadataV2.d.ts b/packages/hasura/src/hasura-metadata-dist/HasuraMetadataV2.d.ts new file mode 100644 index 000000000..009512884 --- /dev/null +++ b/packages/hasura/src/hasura-metadata-dist/HasuraMetadataV2.d.ts @@ -0,0 +1,1227 @@ +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/syntax-defs.html#headerfromvalue + */ +export interface HeaderFromValue { + /** + * Name of the header + */ + name: string; + /** + * Value of the header + */ + value: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/syntax-defs.html#headerfromenv + */ +export interface HeaderFromEnv { + /** + * Name of the header + */ + name: string; + /** + * Name of the environment variable which holds the value of the header + */ + value_from_env: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#objectfield + */ +export interface ObjectField { + /** + * Description of the Input object type + */ + description?: string; + /** + * Name of the Input object type + */ + name: string; + /** + * GraphQL type of the Input object type + */ + type: string; +} +/** + * Type used in exported 'metadata.json' and replace metadata endpoint + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/manage-metadata.html#replace-metadata + */ +export interface HasuraMetadataV2 { + actions?: Action[]; + allowlist?: AllowList[]; + cron_triggers?: CronTrigger[]; + custom_types?: CustomTypes; + functions?: CustomFunction[]; + query_collections?: QueryCollectionEntry[]; + remote_schemas?: RemoteSchema[]; + tables: TableEntry[]; + version: number; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/actions.html#args-syntax + */ +export interface Action { + /** + * Comment + */ + comment?: string; + /** + * Definition of the action + */ + definition: ActionDefinition; + /** + * Name of the action + */ + name: string; + /** + * Permissions of the action + */ + permissions?: Permissions; +} +/** + * Definition of the action + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/actions.html#actiondefinition + */ +export interface ActionDefinition { + arguments?: InputArgument[]; + forward_client_headers?: boolean; + /** + * A String value which supports templating environment variables enclosed in {{ and }}. + * Template example: https://{{ACTION_API_DOMAIN}}/create-user + */ + handler: string; + headers?: Header[]; + kind?: string; + output_type?: string; + type?: ActionDefinitionType; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/actions.html#inputargument + */ +export interface InputArgument { + name: string; + type: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/syntax-defs.html#headerfromvalue + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/syntax-defs.html#headerfromenv + */ +export interface Header { + /** + * Name of the header + */ + name: string; + /** + * Value of the header + */ + value?: string; + /** + * Name of the environment variable which holds the value of the header + */ + value_from_env?: string; +} +export declare enum ActionDefinitionType { + Mutation = 'mutation', + Query = 'query', +} +/** + * Permissions of the action + */ +export interface Permissions { + role: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/query-collections.html#add-collection-to-allowlist-syntax + */ +export interface AllowList { + /** + * Name of a query collection to be added to the allow-list + */ + collection: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/scheduled-triggers.html#create-cron-trigger + */ +export interface CronTrigger { + /** + * Custom comment. + */ + comment?: string; + /** + * List of headers to be sent with the webhook + */ + headers: Header[]; + /** + * Flag to indicate whether a trigger should be included in the metadata. When a cron + * trigger is included in the metadata, the user will be able to export it when the metadata + * of the graphql-engine is exported. + */ + include_in_metadata: boolean; + /** + * Name of the cron trigger + */ + name: string; + /** + * Any JSON payload which will be sent when the webhook is invoked. + */ + payload?: { + [key: string]: any; + }; + /** + * Retry configuration if scheduled invocation delivery fails + */ + retry_conf?: RetryConfST; + /** + * Cron expression at which the trigger should be invoked. + */ + schedule: string; + /** + * URL of the webhook + */ + webhook: string; +} +/** + * Retry configuration if scheduled invocation delivery fails + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/scheduled-triggers.html#retryconfst + */ +export interface RetryConfST { + /** + * Number of times to retry delivery. + * Default: 0 + */ + num_retries?: number; + /** + * Number of seconds to wait between each retry. + * Default: 10 + */ + retry_interval_seconds?: number; + /** + * Number of seconds to wait for response before timing out. + * Default: 60 + */ + timeout_seconds?: number; + /** + * Number of seconds between scheduled time and actual delivery time that is acceptable. If + * the time difference is more than this, then the event is dropped. + * Default: 21600 (6 hours) + */ + tolerance_seconds?: number; +} +export interface CustomTypes { + enums?: EnumType[]; + input_objects?: InputObjectType[]; + objects?: ObjectType[]; + scalars?: ScalarType[]; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#enumtype + */ +export interface EnumType { + /** + * Description of the Enum type + */ + description?: string; + /** + * Name of the Enum type + */ + name: string; + /** + * Values of the Enum type + */ + values: EnumValue[]; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#enumvalue + */ +export interface EnumValue { + /** + * Description of the Enum value + */ + description?: string; + /** + * If set to true, the enum value is marked as deprecated + */ + is_deprecated?: boolean; + /** + * Value of the Enum type + */ + value: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#inputobjecttype + */ +export interface InputObjectType { + /** + * Description of the Input object type + */ + description?: string; + /** + * Fields of the Input object type + */ + fields: InputObjectField[]; + /** + * Name of the Input object type + */ + name: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#inputobjectfield + */ +export interface InputObjectField { + /** + * Description of the Input object type + */ + description?: string; + /** + * Name of the Input object type + */ + name: string; + /** + * GraphQL type of the Input object type + */ + type: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#objecttype + */ +export interface ObjectType { + /** + * Description of the Input object type + */ + description?: string; + /** + * Fields of the Input object type + */ + fields: InputObjectField[]; + /** + * Name of the Input object type + */ + name: string; + /** + * Relationships of the Object type to tables + */ + relationships?: CustomTypeObjectRelationship[]; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#objectrelationship + */ +export interface CustomTypeObjectRelationship { + /** + * Mapping of fields of object type to columns of remote table + */ + field_mapping: { + [key: string]: string; + }; + /** + * Name of the relationship, shouldn’t conflict with existing field names + */ + name: string; + /** + * The table to which relationship is defined + */ + remote_table: QualifiedTable | string; + /** + * Type of the relationship + */ + type: CustomTypeObjectRelationshipType; +} +export interface QualifiedTable { + name: string; + schema: string; +} +/** + * Type of the relationship + */ +export declare enum CustomTypeObjectRelationshipType { + Array = 'array', + Object = 'object', +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-types.html#scalartype + */ +export interface ScalarType { + /** + * Description of the Scalar type + */ + description?: string; + /** + * Name of the Scalar type + */ + name: string; +} +/** + * A custom SQL function to add to the GraphQL schema with configuration. + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-functions.html#args-syntax + */ +export interface CustomFunction { + /** + * Configuration for the SQL function + */ + configuration?: FunctionConfiguration; + /** + * Name of the SQL function + */ + function: QualifiedFunction | string; +} +/** + * Configuration for the SQL function + * + * Configuration for a CustomFunction + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/custom-functions.html#function-configuration + */ +export interface FunctionConfiguration { + /** + * Function argument which accepts session info JSON + * Currently, only functions which satisfy the following constraints can be exposed over the + * GraphQL API (terminology from Postgres docs): + * - Function behaviour: ONLY `STABLE` or `IMMUTABLE` + * - Return type: MUST be `SETOF ` + * - Argument modes: ONLY `IN` + */ + session_argument?: string; +} +export interface QualifiedFunction { + name: string; + schema: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/query-collections.html#args-syntax + */ +export interface QueryCollectionEntry { + /** + * Comment + */ + comment?: string; + /** + * List of queries + */ + definition: Definition; + /** + * Name of the query collection + */ + name: string; +} +/** + * List of queries + */ +export interface Definition { + queries: QueryCollection[]; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/syntax-defs.html#collectionquery + */ +export interface QueryCollection { + name: string; + query: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/remote-schemas.html#add-remote-schema + */ +export interface RemoteSchema { + /** + * Comment + */ + comment?: string; + /** + * Name of the remote schema + */ + definition: RemoteSchemaDef; + /** + * Name of the remote schema + */ + name: string; +} +/** + * Name of the remote schema + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/syntax-defs.html#remoteschemadef + */ +export interface RemoteSchemaDef { + forward_client_headers?: boolean; + headers?: Header[]; + timeout_seconds?: number; + url?: string; + url_from_env?: string; +} +/** + * Representation of a table in metadata, 'tables.yaml' and 'metadata.json' + */ +export interface TableEntry { + array_relationships?: ArrayRelationship[]; + computed_fields?: ComputedField[]; + /** + * Configuration for the table/view + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/table-view.html#table-config + */ + configuration?: TableConfig; + delete_permissions?: DeletePermissionEntry[]; + event_triggers?: EventTrigger[]; + insert_permissions?: InsertPermissionEntry[]; + is_enum?: boolean; + object_relationships?: ObjectRelationship[]; + remote_relationships?: RemoteRelationship[]; + select_permissions?: SelectPermissionEntry[]; + table: QualifiedTable; + update_permissions?: UpdatePermissionEntry[]; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/relationship.html#create-array-relationship-syntax + */ +export interface ArrayRelationship { + /** + * Comment + */ + comment?: string; + /** + * Name of the new relationship + */ + name: string; + /** + * Use one of the available ways to define an array relationship + */ + using: ArrRelUsing; +} +/** + * Use one of the available ways to define an array relationship + * + * Use one of the available ways to define an object relationship + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/relationship.html#arrrelusing + */ +export interface ArrRelUsing { + /** + * The column with foreign key constraint + */ + foreign_key_constraint_on?: ArrRelUsingFKeyOn; + /** + * Manual mapping of table and columns + */ + manual_configuration?: ArrRelUsingManualMapping; +} +/** + * The column with foreign key constraint + * + * The column with foreign key constraint + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/relationship.html#arrrelusingfkeyon + */ +export interface ArrRelUsingFKeyOn { + column: string; + table: QualifiedTable | string; +} +/** + * Manual mapping of table and columns + * + * Manual mapping of table and columns + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/relationship.html#arrrelusingmanualmapping + */ +export interface ArrRelUsingManualMapping { + /** + * Mapping of columns from current table to remote table + */ + column_mapping: { + [key: string]: string; + }; + /** + * The table to which the relationship has to be established + */ + remote_table: QualifiedTable | string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/computed-field.html#args-syntax + */ +export interface ComputedField { + /** + * Comment + */ + comment?: string; + /** + * The computed field definition + */ + definition: ComputedFieldDefinition; + /** + * Name of the new computed field + */ + name: string; +} +/** + * The computed field definition + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/computed-field.html#computedfielddefinition + */ +export interface ComputedFieldDefinition { + /** + * The SQL function + */ + function: QualifiedFunction | string; + /** + * Name of the argument which accepts the Hasura session object as a JSON/JSONB value. If + * omitted, the Hasura session object is not passed to the function + */ + session_argument?: string; + /** + * Name of the argument which accepts a table row type. If omitted, the first argument is + * considered a table argument + */ + table_argument?: string; +} +/** + * Configuration for the table/view + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/table-view.html#table-config + */ +export interface TableConfig { + /** + * Customise the column names + */ + custom_column_names?: { + [key: string]: string; + }; + /** + * Customise the root fields + */ + custom_root_fields?: CustomRootFields; +} +/** + * Customise the root fields + * + * Customise the root fields + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/table-view.html#custom-root-fields + */ +export interface CustomRootFields { + /** + * Customise the `delete_` root field + */ + delete?: string; + /** + * Customise the `delete__by_pk` root field + */ + delete_by_pk?: string; + /** + * Customise the `insert_` root field + */ + insert?: string; + /** + * Customise the `insert__one` root field + */ + insert_one?: string; + /** + * Customise the `` root field + */ + select?: string; + /** + * Customise the `_aggregate` root field + */ + select_aggregate?: string; + /** + * Customise the `_by_pk` root field + */ + select_by_pk?: string; + /** + * Customise the `update_` root field + */ + update?: string; + /** + * Customise the `update__by_pk` root field + */ + update_by_pk?: string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#create-delete-permission-syntax + */ +export interface DeletePermissionEntry { + /** + * Comment + */ + comment?: string; + /** + * The permission definition + */ + permission: DeletePermission; + /** + * Role + */ + role: string; +} +/** + * The permission definition + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#deletepermission + */ +export interface DeletePermission { + /** + * Only the rows where this precondition holds true are updatable + */ + filter?: { + [key: string]: + | number + | { + [key: string]: any; + } + | string; + }; +} +/** + * NOTE: The metadata type doesn't QUITE match the 'create' arguments here + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#create-event-trigger + */ +export interface EventTrigger { + /** + * The SQL function + */ + definition: EventTriggerDefinition; + /** + * The SQL function + */ + headers?: Header[]; + /** + * Name of the event trigger + */ + name: string; + /** + * The SQL function + */ + retry_conf: RetryConf; + /** + * The SQL function + */ + webhook?: string; + webhook_from_env?: string; +} +/** + * The SQL function + */ +export interface EventTriggerDefinition { + /** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#operationspec + */ + delete?: OperationSpec; + enable_manual: boolean; + /** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#operationspec + */ + insert?: OperationSpec; + /** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#operationspec + */ + update?: OperationSpec; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#operationspec + */ +export interface OperationSpec { + /** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#eventtriggercolumns + */ + columns: string[] | Columns; + /** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#eventtriggercolumns + */ + payload?: string[] | Columns; +} +export declare enum Columns { + Empty = '*', +} +/** + * The SQL function + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/event-triggers.html#retryconf + */ +export interface RetryConf { + /** + * Number of seconds to wait between each retry. + * Default: 10 + */ + interval_sec?: number; + /** + * Number of times to retry delivery. + * Default: 0 + */ + num_retries?: number; + /** + * Number of seconds to wait for response before timing out. + * Default: 60 + */ + timeout_sec?: number; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#args-syntax + */ +export interface InsertPermissionEntry { + /** + * Comment + */ + comment?: string; + /** + * The permission definition + */ + permission: InsertPermission; + /** + * Role + */ + role: string; +} +/** + * The permission definition + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#insertpermission + */ +export interface InsertPermission { + /** + * When set to true the mutation is accessible only if x-hasura-use-backend-only-permissions + * session variable exists + * and is set to true and request is made with x-hasura-admin-secret set if any auth is + * configured + */ + backend_only?: boolean; + /** + * This expression has to hold true for every new row that is inserted + */ + check?: { + [key: string]: + | number + | { + [key: string]: any; + } + | string; + }; + /** + * Can insert into only these columns (or all when '*' is specified) + */ + columns: string[] | Columns; + /** + * Preset values for columns that can be sourced from session variables or static values + */ + set?: { + [key: string]: string; + }; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/relationship.html#args-syntax + */ +export interface ObjectRelationship { + /** + * Comment + */ + comment?: string; + /** + * Name of the new relationship + */ + name: string; + /** + * Use one of the available ways to define an object relationship + */ + using: ObjRelUsing; +} +/** + * Use one of the available ways to define an object relationship + * + * Use one of the available ways to define an object relationship + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/relationship.html#objrelusing + */ +export interface ObjRelUsing { + /** + * The column with foreign key constraint + */ + foreign_key_constraint_on?: string; + /** + * Manual mapping of table and columns + */ + manual_configuration?: ObjRelUsingManualMapping; +} +/** + * Manual mapping of table and columns + * + * Manual mapping of table and columns + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/relationship.html#objrelusingmanualmapping + */ +export interface ObjRelUsingManualMapping { + /** + * Mapping of columns from current table to remote table + */ + column_mapping: { + [key: string]: string; + }; + /** + * The table to which the relationship has to be established + */ + remote_table: QualifiedTable | string; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/remote-relationships.html#args-syntax + */ +export interface RemoteRelationship { + /** + * Definition object + */ + definition: RemoteRelationshipDef; + /** + * Name of the remote relationship + */ + name: string; +} +/** + * Definition object + */ +export interface RemoteRelationshipDef { + /** + * Column(s) in the table that is used for joining with remote schema field. + * All join keys in remote_field must appear here. + */ + hasura_fields: string[]; + /** + * The schema tree ending at the field in remote schema which needs to be joined with. + */ + remote_field: { + [key: string]: RemoteField; + }; + /** + * Name of the remote schema to join with + */ + remote_schema: string; +} +export interface RemoteField { + arguments: { + [key: string]: string; + }; + /** + * A recursive tree structure that points to the field in the remote schema that needs to be + * joined with. + * It is recursive because the remote field maybe nested deeply in the remote schema. + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/remote-relationships.html#remotefield + */ + field?: { + [key: string]: RemoteField; + }; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#create-select-permission-syntax + */ +export interface SelectPermissionEntry { + /** + * Comment + */ + comment?: string; + /** + * The permission definition + */ + permission: SelectPermission; + /** + * Role + */ + role: string; +} +/** + * The permission definition + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#selectpermission + */ +export interface SelectPermission { + /** + * Toggle allowing aggregate queries + */ + allow_aggregations?: boolean; + /** + * Only these columns are selectable (or all when '*' is specified) + */ + columns: string[] | Columns; + /** + * Only these computed fields are selectable + */ + computed_fields?: string[]; + /** + * Only the rows where this precondition holds true are selectable + */ + filter?: { + [key: string]: + | number + | { + [key: string]: any; + } + | string; + }; + /** + * The maximum number of rows that can be returned + */ + limit?: number; +} +/** + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#create-update-permission-syntax + */ +export interface UpdatePermissionEntry { + /** + * Comment + */ + comment?: string; + /** + * The permission definition + */ + permission: UpdatePermission; + /** + * Role + */ + role: string; +} +/** + * The permission definition + * + * + * https://hasura.io/docs/1.0/graphql/manual/api-reference/schema-metadata-api/permission.html#updatepermission + */ +export interface UpdatePermission { + /** + * Postcondition which must be satisfied by rows which have been updated + */ + check?: { + [key: string]: + | number + | { + [key: string]: any; + } + | string; + }; + /** + * Only these columns are selectable (or all when '*' is specified) + */ + columns: string[] | Columns; + /** + * Only the rows where this precondition holds true are updatable + */ + filter?: { + [key: string]: + | number + | { + [key: string]: any; + } + | string; + }; + /** + * Preset values for columns that can be sourced from session variables or static values + */ + set?: { + [key: string]: string; + }; +} +export declare class Convert { + static toPGColumn(json: string): string; + static pGColumnToJson(value: string): string; + static toComputedFieldName(json: string): string; + static computedFieldNameToJson(value: string): string; + static toRoleName(json: string): string; + static roleNameToJson(value: string): string; + static toTriggerName(json: string): string; + static triggerNameToJson(value: string): string; + static toRemoteRelationshipName(json: string): string; + static remoteRelationshipNameToJson(value: string): string; + static toRemoteSchemaName(json: string): string; + static remoteSchemaNameToJson(value: string): string; + static toCollectionName(json: string): string; + static collectionNameToJson(value: string): string; + static toGraphQLName(json: string): string; + static graphQLNameToJson(value: string): string; + static toGraphQLType(json: string): string; + static graphQLTypeToJson(value: string): string; + static toRelationshipName(json: string): string; + static relationshipNameToJson(value: string): string; + static toActionName(json: string): string; + static actionNameToJson(value: string): string; + static toWebhookURL(json: string): string; + static webhookURLToJson(value: string): string; + static toTableName(json: string): QualifiedTable | string; + static tableNameToJson(value: QualifiedTable | string): string; + static toQualifiedTable(json: string): QualifiedTable; + static qualifiedTableToJson(value: QualifiedTable): string; + static toTableConfig(json: string): TableConfig; + static tableConfigToJson(value: TableConfig): string; + static toTableEntry(json: string): TableEntry; + static tableEntryToJson(value: TableEntry): string; + static toCustomRootFields(json: string): CustomRootFields; + static customRootFieldsToJson(value: CustomRootFields): string; + static toCustomColumnNames( + json: string + ): { + [key: string]: string; + }; + static customColumnNamesToJson(value: { [key: string]: string }): string; + static toFunctionName(json: string): QualifiedFunction | string; + static functionNameToJson(value: QualifiedFunction | string): string; + static toQualifiedFunction(json: string): QualifiedFunction; + static qualifiedFunctionToJson(value: QualifiedFunction): string; + static toCustomFunction(json: string): CustomFunction; + static customFunctionToJson(value: CustomFunction): string; + static toFunctionConfiguration(json: string): FunctionConfiguration; + static functionConfigurationToJson(value: FunctionConfiguration): string; + static toObjectRelationship(json: string): ObjectRelationship; + static objectRelationshipToJson(value: ObjectRelationship): string; + static toObjRelUsing(json: string): ObjRelUsing; + static objRelUsingToJson(value: ObjRelUsing): string; + static toObjRelUsingManualMapping(json: string): ObjRelUsingManualMapping; + static objRelUsingManualMappingToJson( + value: ObjRelUsingManualMapping + ): string; + static toArrayRelationship(json: string): ArrayRelationship; + static arrayRelationshipToJson(value: ArrayRelationship): string; + static toArrRelUsing(json: string): ArrRelUsing; + static arrRelUsingToJson(value: ArrRelUsing): string; + static toArrRelUsingFKeyOn(json: string): ArrRelUsingFKeyOn; + static arrRelUsingFKeyOnToJson(value: ArrRelUsingFKeyOn): string; + static toArrRelUsingManualMapping(json: string): ArrRelUsingManualMapping; + static arrRelUsingManualMappingToJson( + value: ArrRelUsingManualMapping + ): string; + static toColumnPresetsExpression( + json: string + ): { + [key: string]: string; + }; + static columnPresetsExpressionToJson(value: { + [key: string]: string; + }): string; + static toInsertPermissionEntry(json: string): InsertPermissionEntry; + static insertPermissionEntryToJson(value: InsertPermissionEntry): string; + static toInsertPermission(json: string): InsertPermission; + static insertPermissionToJson(value: InsertPermission): string; + static toSelectPermissionEntry(json: string): SelectPermissionEntry; + static selectPermissionEntryToJson(value: SelectPermissionEntry): string; + static toSelectPermission(json: string): SelectPermission; + static selectPermissionToJson(value: SelectPermission): string; + static toUpdatePermissionEntry(json: string): UpdatePermissionEntry; + static updatePermissionEntryToJson(value: UpdatePermissionEntry): string; + static toUpdatePermission(json: string): UpdatePermission; + static updatePermissionToJson(value: UpdatePermission): string; + static toDeletePermissionEntry(json: string): DeletePermissionEntry; + static deletePermissionEntryToJson(value: DeletePermissionEntry): string; + static toDeletePermission(json: string): DeletePermission; + static deletePermissionToJson(value: DeletePermission): string; + static toComputedField(json: string): ComputedField; + static computedFieldToJson(value: ComputedField): string; + static toComputedFieldDefinition(json: string): ComputedFieldDefinition; + static computedFieldDefinitionToJson(value: ComputedFieldDefinition): string; + static toEventTrigger(json: string): EventTrigger; + static eventTriggerToJson(value: EventTrigger): string; + static toEventTriggerDefinition(json: string): EventTriggerDefinition; + static eventTriggerDefinitionToJson(value: EventTriggerDefinition): string; + static toEventTriggerColumns(json: string): string[] | Columns; + static eventTriggerColumnsToJson(value: string[] | Columns): string; + static toOperationSpec(json: string): OperationSpec; + static operationSpecToJson(value: OperationSpec): string; + static toHeaderFromValue(json: string): HeaderFromValue; + static headerFromValueToJson(value: HeaderFromValue): string; + static toHeaderFromEnv(json: string): HeaderFromEnv; + static headerFromEnvToJson(value: HeaderFromEnv): string; + static toRetryConf(json: string): RetryConf; + static retryConfToJson(value: RetryConf): string; + static toCronTrigger(json: string): CronTrigger; + static cronTriggerToJson(value: CronTrigger): string; + static toRetryConfST(json: string): RetryConfST; + static retryConfSTToJson(value: RetryConfST): string; + static toRemoteSchema(json: string): RemoteSchema; + static remoteSchemaToJson(value: RemoteSchema): string; + static toRemoteSchemaDef(json: string): RemoteSchemaDef; + static remoteSchemaDefToJson(value: RemoteSchemaDef): string; + static toRemoteRelationship(json: string): RemoteRelationship; + static remoteRelationshipToJson(value: RemoteRelationship): string; + static toRemoteRelationshipDef(json: string): RemoteRelationshipDef; + static remoteRelationshipDefToJson(value: RemoteRelationshipDef): string; + static toRemoteField( + json: string + ): { + [key: string]: RemoteField; + }; + static remoteFieldToJson(value: { [key: string]: RemoteField }): string; + static toInputArguments( + json: string + ): { + [key: string]: string; + }; + static inputArgumentsToJson(value: { [key: string]: string }): string; + static toQueryCollectionEntry(json: string): QueryCollectionEntry; + static queryCollectionEntryToJson(value: QueryCollectionEntry): string; + static toQueryCollection(json: string): QueryCollection; + static queryCollectionToJson(value: QueryCollection): string; + static toAllowList(json: string): AllowList; + static allowListToJson(value: AllowList): string; + static toCustomTypes(json: string): CustomTypes; + static customTypesToJson(value: CustomTypes): string; + static toInputObjectType(json: string): InputObjectType; + static inputObjectTypeToJson(value: InputObjectType): string; + static toInputObjectField(json: string): InputObjectField; + static inputObjectFieldToJson(value: InputObjectField): string; + static toObjectType(json: string): ObjectType; + static objectTypeToJson(value: ObjectType): string; + static toObjectField(json: string): ObjectField; + static objectFieldToJson(value: ObjectField): string; + static toCustomTypeObjectRelationship( + json: string + ): CustomTypeObjectRelationship; + static customTypeObjectRelationshipToJson( + value: CustomTypeObjectRelationship + ): string; + static toScalarType(json: string): ScalarType; + static scalarTypeToJson(value: ScalarType): string; + static toEnumType(json: string): EnumType; + static enumTypeToJson(value: EnumType): string; + static toEnumValue(json: string): EnumValue; + static enumValueToJson(value: EnumValue): string; + static toAction(json: string): Action; + static actionToJson(value: Action): string; + static toActionDefinition(json: string): ActionDefinition; + static actionDefinitionToJson(value: ActionDefinition): string; + static toInputArgument(json: string): InputArgument; + static inputArgumentToJson(value: InputArgument): string; + static toHasuraMetadataV2(json: string): HasuraMetadataV2; + static hasuraMetadataV2ToJson(value: HasuraMetadataV2): string; +} diff --git a/packages/hasura/src/hasura-metadata-dist/HasuraMetadataV2.js b/packages/hasura/src/hasura-metadata-dist/HasuraMetadataV2.js new file mode 100644 index 000000000..ec96bdee8 --- /dev/null +++ b/packages/hasura/src/hasura-metadata-dist/HasuraMetadataV2.js @@ -0,0 +1,961 @@ +/* eslint-disable */ +"use strict"; +// To parse this data: +// +// import { Convert, TableName, QualifiedTable, TableConfig, TableEntry, CustomRootFields, FunctionName, QualifiedFunction, CustomFunction, FunctionConfiguration, ObjectRelationship, ObjRelUsing, ObjRelUsingManualMapping, ArrayRelationship, ArrRelUsing, ArrRelUsingFKeyOn, ArrRelUsingManualMapping, InsertPermissionEntry, InsertPermission, SelectPermissionEntry, SelectPermission, UpdatePermissionEntry, UpdatePermission, DeletePermissionEntry, DeletePermission, ComputedField, ComputedFieldDefinition, EventTrigger, EventTriggerDefinition, EventTriggerColumns, OperationSpec, HeaderFromValue, HeaderFromEnv, RetryConf, CronTrigger, RetryConfST, RemoteSchema, RemoteSchemaDef, RemoteRelationship, RemoteRelationshipDef, QueryCollectionEntry, QueryCollection, AllowList, CustomTypes, InputObjectType, InputObjectField, ObjectType, ObjectField, CustomTypeObjectRelationship, ScalarType, EnumType, EnumValue, Action, ActionDefinition, InputArgument, HasuraMetadataV2 } from "./file"; +// +// const pGColumn = Convert.toPGColumn(json); +// const computedFieldName = Convert.toComputedFieldName(json); +// const roleName = Convert.toRoleName(json); +// const triggerName = Convert.toTriggerName(json); +// const remoteRelationshipName = Convert.toRemoteRelationshipName(json); +// const remoteSchemaName = Convert.toRemoteSchemaName(json); +// const collectionName = Convert.toCollectionName(json); +// const graphQLName = Convert.toGraphQLName(json); +// const graphQLType = Convert.toGraphQLType(json); +// const relationshipName = Convert.toRelationshipName(json); +// const actionName = Convert.toActionName(json); +// const webhookURL = Convert.toWebhookURL(json); +// const tableName = Convert.toTableName(json); +// const qualifiedTable = Convert.toQualifiedTable(json); +// const tableConfig = Convert.toTableConfig(json); +// const tableEntry = Convert.toTableEntry(json); +// const customRootFields = Convert.toCustomRootFields(json); +// const customColumnNames = Convert.toCustomColumnNames(json); +// const functionName = Convert.toFunctionName(json); +// const qualifiedFunction = Convert.toQualifiedFunction(json); +// const customFunction = Convert.toCustomFunction(json); +// const functionConfiguration = Convert.toFunctionConfiguration(json); +// const objectRelationship = Convert.toObjectRelationship(json); +// const objRelUsing = Convert.toObjRelUsing(json); +// const objRelUsingManualMapping = Convert.toObjRelUsingManualMapping(json); +// const arrayRelationship = Convert.toArrayRelationship(json); +// const arrRelUsing = Convert.toArrRelUsing(json); +// const arrRelUsingFKeyOn = Convert.toArrRelUsingFKeyOn(json); +// const arrRelUsingManualMapping = Convert.toArrRelUsingManualMapping(json); +// const columnPresetsExpression = Convert.toColumnPresetsExpression(json); +// const insertPermissionEntry = Convert.toInsertPermissionEntry(json); +// const insertPermission = Convert.toInsertPermission(json); +// const selectPermissionEntry = Convert.toSelectPermissionEntry(json); +// const selectPermission = Convert.toSelectPermission(json); +// const updatePermissionEntry = Convert.toUpdatePermissionEntry(json); +// const updatePermission = Convert.toUpdatePermission(json); +// const deletePermissionEntry = Convert.toDeletePermissionEntry(json); +// const deletePermission = Convert.toDeletePermission(json); +// const computedField = Convert.toComputedField(json); +// const computedFieldDefinition = Convert.toComputedFieldDefinition(json); +// const eventTrigger = Convert.toEventTrigger(json); +// const eventTriggerDefinition = Convert.toEventTriggerDefinition(json); +// const eventTriggerColumns = Convert.toEventTriggerColumns(json); +// const operationSpec = Convert.toOperationSpec(json); +// const headerFromValue = Convert.toHeaderFromValue(json); +// const headerFromEnv = Convert.toHeaderFromEnv(json); +// const retryConf = Convert.toRetryConf(json); +// const cronTrigger = Convert.toCronTrigger(json); +// const retryConfST = Convert.toRetryConfST(json); +// const remoteSchema = Convert.toRemoteSchema(json); +// const remoteSchemaDef = Convert.toRemoteSchemaDef(json); +// const remoteRelationship = Convert.toRemoteRelationship(json); +// const remoteRelationshipDef = Convert.toRemoteRelationshipDef(json); +// const remoteField = Convert.toRemoteField(json); +// const inputArguments = Convert.toInputArguments(json); +// const queryCollectionEntry = Convert.toQueryCollectionEntry(json); +// const queryCollection = Convert.toQueryCollection(json); +// const allowList = Convert.toAllowList(json); +// const customTypes = Convert.toCustomTypes(json); +// const inputObjectType = Convert.toInputObjectType(json); +// const inputObjectField = Convert.toInputObjectField(json); +// const objectType = Convert.toObjectType(json); +// const objectField = Convert.toObjectField(json); +// const customTypeObjectRelationship = Convert.toCustomTypeObjectRelationship(json); +// const scalarType = Convert.toScalarType(json); +// const enumType = Convert.toEnumType(json); +// const enumValue = Convert.toEnumValue(json); +// const action = Convert.toAction(json); +// const actionDefinition = Convert.toActionDefinition(json); +// const inputArgument = Convert.toInputArgument(json); +// const hasuraMetadataV2 = Convert.toHasuraMetadataV2(json); +// +// These functions will throw an error if the JSON doesn't +// match the expected interface, even if the JSON is valid. +Object.defineProperty(exports, "__esModule", { value: true }); +var ActionDefinitionType; +(function (ActionDefinitionType) { + ActionDefinitionType["Mutation"] = "mutation"; + ActionDefinitionType["Query"] = "query"; +})(ActionDefinitionType = exports.ActionDefinitionType || (exports.ActionDefinitionType = {})); +/** + * Type of the relationship + */ +var CustomTypeObjectRelationshipType; +(function (CustomTypeObjectRelationshipType) { + CustomTypeObjectRelationshipType["Array"] = "array"; + CustomTypeObjectRelationshipType["Object"] = "object"; +})(CustomTypeObjectRelationshipType = exports.CustomTypeObjectRelationshipType || (exports.CustomTypeObjectRelationshipType = {})); +var Columns; +(function (Columns) { + Columns["Empty"] = "*"; +})(Columns = exports.Columns || (exports.Columns = {})); +// Converts JSON strings to/from your types +// and asserts the results of JSON.parse at runtime +class Convert { + static toPGColumn(json) { + return cast(JSON.parse(json), ""); + } + static pGColumnToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toComputedFieldName(json) { + return cast(JSON.parse(json), ""); + } + static computedFieldNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toRoleName(json) { + return cast(JSON.parse(json), ""); + } + static roleNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toTriggerName(json) { + return cast(JSON.parse(json), ""); + } + static triggerNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toRemoteRelationshipName(json) { + return cast(JSON.parse(json), ""); + } + static remoteRelationshipNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toRemoteSchemaName(json) { + return cast(JSON.parse(json), ""); + } + static remoteSchemaNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toCollectionName(json) { + return cast(JSON.parse(json), ""); + } + static collectionNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toGraphQLName(json) { + return cast(JSON.parse(json), ""); + } + static graphQLNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toGraphQLType(json) { + return cast(JSON.parse(json), ""); + } + static graphQLTypeToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toRelationshipName(json) { + return cast(JSON.parse(json), ""); + } + static relationshipNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toActionName(json) { + return cast(JSON.parse(json), ""); + } + static actionNameToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toWebhookURL(json) { + return cast(JSON.parse(json), ""); + } + static webhookURLToJson(value) { + return JSON.stringify(uncast(value, ""), null, 2); + } + static toTableName(json) { + return cast(JSON.parse(json), u(r("QualifiedTable"), "")); + } + static tableNameToJson(value) { + return JSON.stringify(uncast(value, u(r("QualifiedTable"), "")), null, 2); + } + static toQualifiedTable(json) { + return cast(JSON.parse(json), r("QualifiedTable")); + } + static qualifiedTableToJson(value) { + return JSON.stringify(uncast(value, r("QualifiedTable")), null, 2); + } + static toTableConfig(json) { + return cast(JSON.parse(json), r("TableConfig")); + } + static tableConfigToJson(value) { + return JSON.stringify(uncast(value, r("TableConfig")), null, 2); + } + static toTableEntry(json) { + return cast(JSON.parse(json), r("TableEntry")); + } + static tableEntryToJson(value) { + return JSON.stringify(uncast(value, r("TableEntry")), null, 2); + } + static toCustomRootFields(json) { + return cast(JSON.parse(json), r("CustomRootFields")); + } + static customRootFieldsToJson(value) { + return JSON.stringify(uncast(value, r("CustomRootFields")), null, 2); + } + static toCustomColumnNames(json) { + return cast(JSON.parse(json), m("")); + } + static customColumnNamesToJson(value) { + return JSON.stringify(uncast(value, m("")), null, 2); + } + static toFunctionName(json) { + return cast(JSON.parse(json), u(r("QualifiedFunction"), "")); + } + static functionNameToJson(value) { + return JSON.stringify(uncast(value, u(r("QualifiedFunction"), "")), null, 2); + } + static toQualifiedFunction(json) { + return cast(JSON.parse(json), r("QualifiedFunction")); + } + static qualifiedFunctionToJson(value) { + return JSON.stringify(uncast(value, r("QualifiedFunction")), null, 2); + } + static toCustomFunction(json) { + return cast(JSON.parse(json), r("CustomFunction")); + } + static customFunctionToJson(value) { + return JSON.stringify(uncast(value, r("CustomFunction")), null, 2); + } + static toFunctionConfiguration(json) { + return cast(JSON.parse(json), r("FunctionConfiguration")); + } + static functionConfigurationToJson(value) { + return JSON.stringify(uncast(value, r("FunctionConfiguration")), null, 2); + } + static toObjectRelationship(json) { + return cast(JSON.parse(json), r("ObjectRelationship")); + } + static objectRelationshipToJson(value) { + return JSON.stringify(uncast(value, r("ObjectRelationship")), null, 2); + } + static toObjRelUsing(json) { + return cast(JSON.parse(json), r("ObjRelUsing")); + } + static objRelUsingToJson(value) { + return JSON.stringify(uncast(value, r("ObjRelUsing")), null, 2); + } + static toObjRelUsingManualMapping(json) { + return cast(JSON.parse(json), r("ObjRelUsingManualMapping")); + } + static objRelUsingManualMappingToJson(value) { + return JSON.stringify(uncast(value, r("ObjRelUsingManualMapping")), null, 2); + } + static toArrayRelationship(json) { + return cast(JSON.parse(json), r("ArrayRelationship")); + } + static arrayRelationshipToJson(value) { + return JSON.stringify(uncast(value, r("ArrayRelationship")), null, 2); + } + static toArrRelUsing(json) { + return cast(JSON.parse(json), r("ArrRelUsing")); + } + static arrRelUsingToJson(value) { + return JSON.stringify(uncast(value, r("ArrRelUsing")), null, 2); + } + static toArrRelUsingFKeyOn(json) { + return cast(JSON.parse(json), r("ArrRelUsingFKeyOn")); + } + static arrRelUsingFKeyOnToJson(value) { + return JSON.stringify(uncast(value, r("ArrRelUsingFKeyOn")), null, 2); + } + static toArrRelUsingManualMapping(json) { + return cast(JSON.parse(json), r("ArrRelUsingManualMapping")); + } + static arrRelUsingManualMappingToJson(value) { + return JSON.stringify(uncast(value, r("ArrRelUsingManualMapping")), null, 2); + } + static toColumnPresetsExpression(json) { + return cast(JSON.parse(json), m("")); + } + static columnPresetsExpressionToJson(value) { + return JSON.stringify(uncast(value, m("")), null, 2); + } + static toInsertPermissionEntry(json) { + return cast(JSON.parse(json), r("InsertPermissionEntry")); + } + static insertPermissionEntryToJson(value) { + return JSON.stringify(uncast(value, r("InsertPermissionEntry")), null, 2); + } + static toInsertPermission(json) { + return cast(JSON.parse(json), r("InsertPermission")); + } + static insertPermissionToJson(value) { + return JSON.stringify(uncast(value, r("InsertPermission")), null, 2); + } + static toSelectPermissionEntry(json) { + return cast(JSON.parse(json), r("SelectPermissionEntry")); + } + static selectPermissionEntryToJson(value) { + return JSON.stringify(uncast(value, r("SelectPermissionEntry")), null, 2); + } + static toSelectPermission(json) { + return cast(JSON.parse(json), r("SelectPermission")); + } + static selectPermissionToJson(value) { + return JSON.stringify(uncast(value, r("SelectPermission")), null, 2); + } + static toUpdatePermissionEntry(json) { + return cast(JSON.parse(json), r("UpdatePermissionEntry")); + } + static updatePermissionEntryToJson(value) { + return JSON.stringify(uncast(value, r("UpdatePermissionEntry")), null, 2); + } + static toUpdatePermission(json) { + return cast(JSON.parse(json), r("UpdatePermission")); + } + static updatePermissionToJson(value) { + return JSON.stringify(uncast(value, r("UpdatePermission")), null, 2); + } + static toDeletePermissionEntry(json) { + return cast(JSON.parse(json), r("DeletePermissionEntry")); + } + static deletePermissionEntryToJson(value) { + return JSON.stringify(uncast(value, r("DeletePermissionEntry")), null, 2); + } + static toDeletePermission(json) { + return cast(JSON.parse(json), r("DeletePermission")); + } + static deletePermissionToJson(value) { + return JSON.stringify(uncast(value, r("DeletePermission")), null, 2); + } + static toComputedField(json) { + return cast(JSON.parse(json), r("ComputedField")); + } + static computedFieldToJson(value) { + return JSON.stringify(uncast(value, r("ComputedField")), null, 2); + } + static toComputedFieldDefinition(json) { + return cast(JSON.parse(json), r("ComputedFieldDefinition")); + } + static computedFieldDefinitionToJson(value) { + return JSON.stringify(uncast(value, r("ComputedFieldDefinition")), null, 2); + } + static toEventTrigger(json) { + return cast(JSON.parse(json), r("EventTrigger")); + } + static eventTriggerToJson(value) { + return JSON.stringify(uncast(value, r("EventTrigger")), null, 2); + } + static toEventTriggerDefinition(json) { + return cast(JSON.parse(json), r("EventTriggerDefinition")); + } + static eventTriggerDefinitionToJson(value) { + return JSON.stringify(uncast(value, r("EventTriggerDefinition")), null, 2); + } + static toEventTriggerColumns(json) { + return cast(JSON.parse(json), u(a(""), r("Columns"))); + } + static eventTriggerColumnsToJson(value) { + return JSON.stringify(uncast(value, u(a(""), r("Columns"))), null, 2); + } + static toOperationSpec(json) { + return cast(JSON.parse(json), r("OperationSpec")); + } + static operationSpecToJson(value) { + return JSON.stringify(uncast(value, r("OperationSpec")), null, 2); + } + static toHeaderFromValue(json) { + return cast(JSON.parse(json), r("HeaderFromValue")); + } + static headerFromValueToJson(value) { + return JSON.stringify(uncast(value, r("HeaderFromValue")), null, 2); + } + static toHeaderFromEnv(json) { + return cast(JSON.parse(json), r("HeaderFromEnv")); + } + static headerFromEnvToJson(value) { + return JSON.stringify(uncast(value, r("HeaderFromEnv")), null, 2); + } + static toRetryConf(json) { + return cast(JSON.parse(json), r("RetryConf")); + } + static retryConfToJson(value) { + return JSON.stringify(uncast(value, r("RetryConf")), null, 2); + } + static toCronTrigger(json) { + return cast(JSON.parse(json), r("CronTrigger")); + } + static cronTriggerToJson(value) { + return JSON.stringify(uncast(value, r("CronTrigger")), null, 2); + } + static toRetryConfST(json) { + return cast(JSON.parse(json), r("RetryConfST")); + } + static retryConfSTToJson(value) { + return JSON.stringify(uncast(value, r("RetryConfST")), null, 2); + } + static toRemoteSchema(json) { + return cast(JSON.parse(json), r("RemoteSchema")); + } + static remoteSchemaToJson(value) { + return JSON.stringify(uncast(value, r("RemoteSchema")), null, 2); + } + static toRemoteSchemaDef(json) { + return cast(JSON.parse(json), r("RemoteSchemaDef")); + } + static remoteSchemaDefToJson(value) { + return JSON.stringify(uncast(value, r("RemoteSchemaDef")), null, 2); + } + static toRemoteRelationship(json) { + return cast(JSON.parse(json), r("RemoteRelationship")); + } + static remoteRelationshipToJson(value) { + return JSON.stringify(uncast(value, r("RemoteRelationship")), null, 2); + } + static toRemoteRelationshipDef(json) { + return cast(JSON.parse(json), r("RemoteRelationshipDef")); + } + static remoteRelationshipDefToJson(value) { + return JSON.stringify(uncast(value, r("RemoteRelationshipDef")), null, 2); + } + static toRemoteField(json) { + return cast(JSON.parse(json), m(r("RemoteField"))); + } + static remoteFieldToJson(value) { + return JSON.stringify(uncast(value, m(r("RemoteField"))), null, 2); + } + static toInputArguments(json) { + return cast(JSON.parse(json), m("")); + } + static inputArgumentsToJson(value) { + return JSON.stringify(uncast(value, m("")), null, 2); + } + static toQueryCollectionEntry(json) { + return cast(JSON.parse(json), r("QueryCollectionEntry")); + } + static queryCollectionEntryToJson(value) { + return JSON.stringify(uncast(value, r("QueryCollectionEntry")), null, 2); + } + static toQueryCollection(json) { + return cast(JSON.parse(json), r("QueryCollection")); + } + static queryCollectionToJson(value) { + return JSON.stringify(uncast(value, r("QueryCollection")), null, 2); + } + static toAllowList(json) { + return cast(JSON.parse(json), r("AllowList")); + } + static allowListToJson(value) { + return JSON.stringify(uncast(value, r("AllowList")), null, 2); + } + static toCustomTypes(json) { + return cast(JSON.parse(json), r("CustomTypes")); + } + static customTypesToJson(value) { + return JSON.stringify(uncast(value, r("CustomTypes")), null, 2); + } + static toInputObjectType(json) { + return cast(JSON.parse(json), r("InputObjectType")); + } + static inputObjectTypeToJson(value) { + return JSON.stringify(uncast(value, r("InputObjectType")), null, 2); + } + static toInputObjectField(json) { + return cast(JSON.parse(json), r("InputObjectField")); + } + static inputObjectFieldToJson(value) { + return JSON.stringify(uncast(value, r("InputObjectField")), null, 2); + } + static toObjectType(json) { + return cast(JSON.parse(json), r("ObjectType")); + } + static objectTypeToJson(value) { + return JSON.stringify(uncast(value, r("ObjectType")), null, 2); + } + static toObjectField(json) { + return cast(JSON.parse(json), r("ObjectField")); + } + static objectFieldToJson(value) { + return JSON.stringify(uncast(value, r("ObjectField")), null, 2); + } + static toCustomTypeObjectRelationship(json) { + return cast(JSON.parse(json), r("CustomTypeObjectRelationship")); + } + static customTypeObjectRelationshipToJson(value) { + return JSON.stringify(uncast(value, r("CustomTypeObjectRelationship")), null, 2); + } + static toScalarType(json) { + return cast(JSON.parse(json), r("ScalarType")); + } + static scalarTypeToJson(value) { + return JSON.stringify(uncast(value, r("ScalarType")), null, 2); + } + static toEnumType(json) { + return cast(JSON.parse(json), r("EnumType")); + } + static enumTypeToJson(value) { + return JSON.stringify(uncast(value, r("EnumType")), null, 2); + } + static toEnumValue(json) { + return cast(JSON.parse(json), r("EnumValue")); + } + static enumValueToJson(value) { + return JSON.stringify(uncast(value, r("EnumValue")), null, 2); + } + static toAction(json) { + return cast(JSON.parse(json), r("Action")); + } + static actionToJson(value) { + return JSON.stringify(uncast(value, r("Action")), null, 2); + } + static toActionDefinition(json) { + return cast(JSON.parse(json), r("ActionDefinition")); + } + static actionDefinitionToJson(value) { + return JSON.stringify(uncast(value, r("ActionDefinition")), null, 2); + } + static toInputArgument(json) { + return cast(JSON.parse(json), r("InputArgument")); + } + static inputArgumentToJson(value) { + return JSON.stringify(uncast(value, r("InputArgument")), null, 2); + } + static toHasuraMetadataV2(json) { + return cast(JSON.parse(json), r("HasuraMetadataV2")); + } + static hasuraMetadataV2ToJson(value) { + return JSON.stringify(uncast(value, r("HasuraMetadataV2")), null, 2); + } +} +exports.Convert = Convert; +function invalidValue(typ, val) { + throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`); +} +function jsonToJSProps(typ) { + if (typ.jsonToJS === undefined) { + const map = {}; + typ.props.forEach((p) => map[p.json] = { key: p.js, typ: p.typ }); + typ.jsonToJS = map; + } + return typ.jsonToJS; +} +function jsToJSONProps(typ) { + if (typ.jsToJSON === undefined) { + const map = {}; + typ.props.forEach((p) => map[p.js] = { key: p.json, typ: p.typ }); + typ.jsToJSON = map; + } + return typ.jsToJSON; +} +function transform(val, typ, getProps) { + function transformPrimitive(typ, val) { + if (typeof typ === typeof val) + return val; + return invalidValue(typ, val); + } + function transformUnion(typs, val) { + // val must validate against one typ in typs + const l = typs.length; + for (let i = 0; i < l; i++) { + const typ = typs[i]; + try { + return transform(val, typ, getProps); + } + catch (_) { } + } + return invalidValue(typs, val); + } + function transformEnum(cases, val) { + if (cases.indexOf(val) !== -1) + return val; + return invalidValue(cases, val); + } + function transformArray(typ, val) { + // val must be an array with no invalid elements + if (!Array.isArray(val)) + return invalidValue("array", val); + return val.map(el => transform(el, typ, getProps)); + } + function transformDate(val) { + if (val === null) { + return null; + } + const d = new Date(val); + if (isNaN(d.valueOf())) { + return invalidValue("Date", val); + } + return d; + } + function transformObject(props, additional, val) { + if (val === null || typeof val !== "object" || Array.isArray(val)) { + return invalidValue("object", val); + } + const result = {}; + Object.getOwnPropertyNames(props).forEach(key => { + const prop = props[key]; + const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined; + result[prop.key] = transform(v, prop.typ, getProps); + }); + Object.getOwnPropertyNames(val).forEach(key => { + if (!Object.prototype.hasOwnProperty.call(props, key)) { + result[key] = transform(val[key], additional, getProps); + } + }); + return result; + } + if (typ === "any") + return val; + if (typ === null) { + if (val === null) + return val; + return invalidValue(typ, val); + } + if (typ === false) + return invalidValue(typ, val); + while (typeof typ === "object" && typ.ref !== undefined) { + typ = typeMap[typ.ref]; + } + if (Array.isArray(typ)) + return transformEnum(typ, val); + if (typeof typ === "object") { + return typ.hasOwnProperty("unionMembers") ? transformUnion(typ.unionMembers, val) + : typ.hasOwnProperty("arrayItems") ? transformArray(typ.arrayItems, val) + : typ.hasOwnProperty("props") ? transformObject(getProps(typ), typ.additional, val) + : invalidValue(typ, val); + } + // Numbers can be parsed by Date but shouldn't be. + if (typ === Date && typeof val !== "number") + return transformDate(val); + return transformPrimitive(typ, val); +} +function cast(val, typ) { + return transform(val, typ, jsonToJSProps); +} +function uncast(val, typ) { + return transform(val, typ, jsToJSONProps); +} +function a(typ) { + return { arrayItems: typ }; +} +function u(...typs) { + return { unionMembers: typs }; +} +function o(props, additional) { + return { props, additional }; +} +function m(additional) { + return { props: [], additional }; +} +function r(name) { + return { ref: name }; +} +const typeMap = { + "HeaderFromValue": o([ + { json: "name", js: "name", typ: "" }, + { json: "value", js: "value", typ: "" }, + ], "any"), + "HeaderFromEnv": o([ + { json: "name", js: "name", typ: "" }, + { json: "value_from_env", js: "value_from_env", typ: "" }, + ], "any"), + "ObjectField": o([ + { json: "description", js: "description", typ: u(undefined, "") }, + { json: "name", js: "name", typ: "" }, + { json: "type", js: "type", typ: "" }, + ], "any"), + "HasuraMetadataV2": o([ + { json: "actions", js: "actions", typ: u(undefined, a(r("Action"))) }, + { json: "allowlist", js: "allowlist", typ: u(undefined, a(r("AllowList"))) }, + { json: "cron_triggers", js: "cron_triggers", typ: u(undefined, a(r("CronTrigger"))) }, + { json: "custom_types", js: "custom_types", typ: u(undefined, r("CustomTypes")) }, + { json: "functions", js: "functions", typ: u(undefined, a(r("CustomFunction"))) }, + { json: "query_collections", js: "query_collections", typ: u(undefined, a(r("QueryCollectionEntry"))) }, + { json: "remote_schemas", js: "remote_schemas", typ: u(undefined, a(r("RemoteSchema"))) }, + { json: "tables", js: "tables", typ: a(r("TableEntry")) }, + { json: "version", js: "version", typ: 3.14 }, + ], "any"), + "Action": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "definition", js: "definition", typ: r("ActionDefinition") }, + { json: "name", js: "name", typ: "" }, + { json: "permissions", js: "permissions", typ: u(undefined, r("Permissions")) }, + ], "any"), + "ActionDefinition": o([ + { json: "arguments", js: "arguments", typ: u(undefined, a(r("InputArgument"))) }, + { json: "forward_client_headers", js: "forward_client_headers", typ: u(undefined, true) }, + { json: "handler", js: "handler", typ: "" }, + { json: "headers", js: "headers", typ: u(undefined, a(r("Header"))) }, + { json: "kind", js: "kind", typ: u(undefined, "") }, + { json: "output_type", js: "output_type", typ: u(undefined, "") }, + { json: "type", js: "type", typ: u(undefined, r("ActionDefinitionType")) }, + ], "any"), + "InputArgument": o([ + { json: "name", js: "name", typ: "" }, + { json: "type", js: "type", typ: "" }, + ], "any"), + "Header": o([ + { json: "name", js: "name", typ: "" }, + { json: "value", js: "value", typ: u(undefined, "") }, + { json: "value_from_env", js: "value_from_env", typ: u(undefined, "") }, + ], "any"), + "Permissions": o([ + { json: "role", js: "role", typ: "" }, + ], "any"), + "AllowList": o([ + { json: "collection", js: "collection", typ: "" }, + ], "any"), + "CronTrigger": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "headers", js: "headers", typ: a(r("Header")) }, + { json: "include_in_metadata", js: "include_in_metadata", typ: true }, + { json: "name", js: "name", typ: "" }, + { json: "payload", js: "payload", typ: u(undefined, m("any")) }, + { json: "retry_conf", js: "retry_conf", typ: u(undefined, r("RetryConfST")) }, + { json: "schedule", js: "schedule", typ: "" }, + { json: "webhook", js: "webhook", typ: "" }, + ], "any"), + "RetryConfST": o([ + { json: "num_retries", js: "num_retries", typ: u(undefined, 0) }, + { json: "retry_interval_seconds", js: "retry_interval_seconds", typ: u(undefined, 0) }, + { json: "timeout_seconds", js: "timeout_seconds", typ: u(undefined, 0) }, + { json: "tolerance_seconds", js: "tolerance_seconds", typ: u(undefined, 0) }, + ], "any"), + "CustomTypes": o([ + { json: "enums", js: "enums", typ: u(undefined, a(r("EnumType"))) }, + { json: "input_objects", js: "input_objects", typ: u(undefined, a(r("InputObjectType"))) }, + { json: "objects", js: "objects", typ: u(undefined, a(r("ObjectType"))) }, + { json: "scalars", js: "scalars", typ: u(undefined, a(r("ScalarType"))) }, + ], "any"), + "EnumType": o([ + { json: "description", js: "description", typ: u(undefined, "") }, + { json: "name", js: "name", typ: "" }, + { json: "values", js: "values", typ: a(r("EnumValue")) }, + ], "any"), + "EnumValue": o([ + { json: "description", js: "description", typ: u(undefined, "") }, + { json: "is_deprecated", js: "is_deprecated", typ: u(undefined, true) }, + { json: "value", js: "value", typ: "" }, + ], "any"), + "InputObjectType": o([ + { json: "description", js: "description", typ: u(undefined, "") }, + { json: "fields", js: "fields", typ: a(r("InputObjectField")) }, + { json: "name", js: "name", typ: "" }, + ], "any"), + "InputObjectField": o([ + { json: "description", js: "description", typ: u(undefined, "") }, + { json: "name", js: "name", typ: "" }, + { json: "type", js: "type", typ: "" }, + ], "any"), + "ObjectType": o([ + { json: "description", js: "description", typ: u(undefined, "") }, + { json: "fields", js: "fields", typ: a(r("InputObjectField")) }, + { json: "name", js: "name", typ: "" }, + { json: "relationships", js: "relationships", typ: u(undefined, a(r("CustomTypeObjectRelationship"))) }, + ], "any"), + "CustomTypeObjectRelationship": o([ + { json: "field_mapping", js: "field_mapping", typ: m("") }, + { json: "name", js: "name", typ: "" }, + { json: "remote_table", js: "remote_table", typ: u(r("QualifiedTable"), "") }, + { json: "type", js: "type", typ: r("CustomTypeObjectRelationshipType") }, + ], "any"), + "QualifiedTable": o([ + { json: "name", js: "name", typ: "" }, + { json: "schema", js: "schema", typ: "" }, + ], "any"), + "ScalarType": o([ + { json: "description", js: "description", typ: u(undefined, "") }, + { json: "name", js: "name", typ: "" }, + ], "any"), + "CustomFunction": o([ + { json: "configuration", js: "configuration", typ: u(undefined, r("FunctionConfiguration")) }, + { json: "function", js: "function", typ: u(r("QualifiedFunction"), "") }, + ], "any"), + "FunctionConfiguration": o([ + { json: "session_argument", js: "session_argument", typ: u(undefined, "") }, + ], "any"), + "QualifiedFunction": o([ + { json: "name", js: "name", typ: "" }, + { json: "schema", js: "schema", typ: "" }, + ], "any"), + "QueryCollectionEntry": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "definition", js: "definition", typ: r("Definition") }, + { json: "name", js: "name", typ: "" }, + ], "any"), + "Definition": o([ + { json: "queries", js: "queries", typ: a(r("QueryCollection")) }, + ], "any"), + "QueryCollection": o([ + { json: "name", js: "name", typ: "" }, + { json: "query", js: "query", typ: "" }, + ], "any"), + "RemoteSchema": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "definition", js: "definition", typ: r("RemoteSchemaDef") }, + { json: "name", js: "name", typ: "" }, + ], "any"), + "RemoteSchemaDef": o([ + { json: "forward_client_headers", js: "forward_client_headers", typ: u(undefined, true) }, + { json: "headers", js: "headers", typ: u(undefined, a(r("Header"))) }, + { json: "timeout_seconds", js: "timeout_seconds", typ: u(undefined, 3.14) }, + { json: "url", js: "url", typ: u(undefined, "") }, + { json: "url_from_env", js: "url_from_env", typ: u(undefined, "") }, + ], "any"), + "TableEntry": o([ + { json: "array_relationships", js: "array_relationships", typ: u(undefined, a(r("ArrayRelationship"))) }, + { json: "computed_fields", js: "computed_fields", typ: u(undefined, a(r("ComputedField"))) }, + { json: "configuration", js: "configuration", typ: u(undefined, r("TableConfig")) }, + { json: "delete_permissions", js: "delete_permissions", typ: u(undefined, a(r("DeletePermissionEntry"))) }, + { json: "event_triggers", js: "event_triggers", typ: u(undefined, a(r("EventTrigger"))) }, + { json: "insert_permissions", js: "insert_permissions", typ: u(undefined, a(r("InsertPermissionEntry"))) }, + { json: "is_enum", js: "is_enum", typ: u(undefined, true) }, + { json: "object_relationships", js: "object_relationships", typ: u(undefined, a(r("ObjectRelationship"))) }, + { json: "remote_relationships", js: "remote_relationships", typ: u(undefined, a(r("RemoteRelationship"))) }, + { json: "select_permissions", js: "select_permissions", typ: u(undefined, a(r("SelectPermissionEntry"))) }, + { json: "table", js: "table", typ: r("QualifiedTable") }, + { json: "update_permissions", js: "update_permissions", typ: u(undefined, a(r("UpdatePermissionEntry"))) }, + ], "any"), + "ArrayRelationship": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "name", js: "name", typ: "" }, + { json: "using", js: "using", typ: r("ArrRelUsing") }, + ], "any"), + "ArrRelUsing": o([ + { json: "foreign_key_constraint_on", js: "foreign_key_constraint_on", typ: u(undefined, r("ArrRelUsingFKeyOn")) }, + { json: "manual_configuration", js: "manual_configuration", typ: u(undefined, r("ArrRelUsingManualMapping")) }, + ], "any"), + "ArrRelUsingFKeyOn": o([ + { json: "column", js: "column", typ: "" }, + { json: "table", js: "table", typ: u(r("QualifiedTable"), "") }, + ], "any"), + "ArrRelUsingManualMapping": o([ + { json: "column_mapping", js: "column_mapping", typ: m("") }, + { json: "remote_table", js: "remote_table", typ: u(r("QualifiedTable"), "") }, + ], "any"), + "ComputedField": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "definition", js: "definition", typ: r("ComputedFieldDefinition") }, + { json: "name", js: "name", typ: "" }, + ], "any"), + "ComputedFieldDefinition": o([ + { json: "function", js: "function", typ: u(r("QualifiedFunction"), "") }, + { json: "session_argument", js: "session_argument", typ: u(undefined, "") }, + { json: "table_argument", js: "table_argument", typ: u(undefined, "") }, + ], "any"), + "TableConfig": o([ + { json: "custom_column_names", js: "custom_column_names", typ: u(undefined, m("")) }, + { json: "custom_root_fields", js: "custom_root_fields", typ: u(undefined, r("CustomRootFields")) }, + ], "any"), + "CustomRootFields": o([ + { json: "delete", js: "delete", typ: u(undefined, "") }, + { json: "delete_by_pk", js: "delete_by_pk", typ: u(undefined, "") }, + { json: "insert", js: "insert", typ: u(undefined, "") }, + { json: "insert_one", js: "insert_one", typ: u(undefined, "") }, + { json: "select", js: "select", typ: u(undefined, "") }, + { json: "select_aggregate", js: "select_aggregate", typ: u(undefined, "") }, + { json: "select_by_pk", js: "select_by_pk", typ: u(undefined, "") }, + { json: "update", js: "update", typ: u(undefined, "") }, + { json: "update_by_pk", js: "update_by_pk", typ: u(undefined, "") }, + ], "any"), + "DeletePermissionEntry": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "permission", js: "permission", typ: r("DeletePermission") }, + { json: "role", js: "role", typ: "" }, + ], "any"), + "DeletePermission": o([ + { json: "filter", js: "filter", typ: u(undefined, m(u(3.14, m("any"), ""))) }, + ], "any"), + "EventTrigger": o([ + { json: "definition", js: "definition", typ: r("EventTriggerDefinition") }, + { json: "headers", js: "headers", typ: u(undefined, a(r("Header"))) }, + { json: "name", js: "name", typ: "" }, + { json: "retry_conf", js: "retry_conf", typ: r("RetryConf") }, + { json: "webhook", js: "webhook", typ: u(undefined, "") }, + { json: "webhook_from_env", js: "webhook_from_env", typ: u(undefined, "") }, + ], "any"), + "EventTriggerDefinition": o([ + { json: "delete", js: "delete", typ: u(undefined, r("OperationSpec")) }, + { json: "enable_manual", js: "enable_manual", typ: true }, + { json: "insert", js: "insert", typ: u(undefined, r("OperationSpec")) }, + { json: "update", js: "update", typ: u(undefined, r("OperationSpec")) }, + ], "any"), + "OperationSpec": o([ + { json: "columns", js: "columns", typ: u(a(""), r("Columns")) }, + { json: "payload", js: "payload", typ: u(undefined, u(a(""), r("Columns"))) }, + ], "any"), + "RetryConf": o([ + { json: "interval_sec", js: "interval_sec", typ: u(undefined, 0) }, + { json: "num_retries", js: "num_retries", typ: u(undefined, 0) }, + { json: "timeout_sec", js: "timeout_sec", typ: u(undefined, 0) }, + ], "any"), + "InsertPermissionEntry": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "permission", js: "permission", typ: r("InsertPermission") }, + { json: "role", js: "role", typ: "" }, + ], "any"), + "InsertPermission": o([ + { json: "backend_only", js: "backend_only", typ: u(undefined, true) }, + { json: "check", js: "check", typ: u(undefined, m(u(3.14, m("any"), ""))) }, + { json: "columns", js: "columns", typ: u(a(""), r("Columns")) }, + { json: "set", js: "set", typ: u(undefined, m("")) }, + ], "any"), + "ObjectRelationship": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "name", js: "name", typ: "" }, + { json: "using", js: "using", typ: r("ObjRelUsing") }, + ], "any"), + "ObjRelUsing": o([ + { json: "foreign_key_constraint_on", js: "foreign_key_constraint_on", typ: u(undefined, "") }, + { json: "manual_configuration", js: "manual_configuration", typ: u(undefined, r("ObjRelUsingManualMapping")) }, + ], "any"), + "ObjRelUsingManualMapping": o([ + { json: "column_mapping", js: "column_mapping", typ: m("") }, + { json: "remote_table", js: "remote_table", typ: u(r("QualifiedTable"), "") }, + ], "any"), + "RemoteRelationship": o([ + { json: "definition", js: "definition", typ: r("RemoteRelationshipDef") }, + { json: "name", js: "name", typ: "" }, + ], "any"), + "RemoteRelationshipDef": o([ + { json: "hasura_fields", js: "hasura_fields", typ: a("") }, + { json: "remote_field", js: "remote_field", typ: m(r("RemoteField")) }, + { json: "remote_schema", js: "remote_schema", typ: "" }, + ], "any"), + "RemoteField": o([ + { json: "arguments", js: "arguments", typ: m("") }, + { json: "field", js: "field", typ: u(undefined, m(r("RemoteField"))) }, + ], "any"), + "SelectPermissionEntry": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "permission", js: "permission", typ: r("SelectPermission") }, + { json: "role", js: "role", typ: "" }, + ], "any"), + "SelectPermission": o([ + { json: "allow_aggregations", js: "allow_aggregations", typ: u(undefined, true) }, + { json: "columns", js: "columns", typ: u(a(""), r("Columns")) }, + { json: "computed_fields", js: "computed_fields", typ: u(undefined, a("")) }, + { json: "filter", js: "filter", typ: u(undefined, m(u(3.14, m("any"), ""))) }, + { json: "limit", js: "limit", typ: u(undefined, 0) }, + ], "any"), + "UpdatePermissionEntry": o([ + { json: "comment", js: "comment", typ: u(undefined, "") }, + { json: "permission", js: "permission", typ: r("UpdatePermission") }, + { json: "role", js: "role", typ: "" }, + ], "any"), + "UpdatePermission": o([ + { json: "check", js: "check", typ: u(undefined, m(u(3.14, m("any"), ""))) }, + { json: "columns", js: "columns", typ: u(a(""), r("Columns")) }, + { json: "filter", js: "filter", typ: u(undefined, m(u(3.14, m("any"), ""))) }, + { json: "set", js: "set", typ: u(undefined, m("")) }, + ], "any"), + "ActionDefinitionType": [ + "mutation", + "query", + ], + "CustomTypeObjectRelationshipType": [ + "array", + "object", + ], + "Columns": [ + "*", + ], +}; diff --git a/packages/hasura/src/hasura.constants.ts b/packages/hasura/src/hasura.constants.ts index f72e7049a..621c23b53 100644 --- a/packages/hasura/src/hasura.constants.ts +++ b/packages/hasura/src/hasura.constants.ts @@ -1,2 +1,5 @@ export const HASURA_EVENT_HANDLER = Symbol('HASURA_EVENT_HANDLER'); export const HASURA_MODULE_CONFIG = Symbol('HASURA_MODULE_CONFIG'); +export const HASURA_SCHEDULED_EVENT_HANDLER = Symbol( + 'HASURA_SCHEDULED_EVENT_HANDLER' +); diff --git a/packages/hasura/src/hasura.decorators.ts b/packages/hasura/src/hasura.decorators.ts index 15e5e59f7..854498a85 100644 --- a/packages/hasura/src/hasura.decorators.ts +++ b/packages/hasura/src/hasura.decorators.ts @@ -1,7 +1,15 @@ import { makeInjectableDecorator } from '@golevelup/nestjs-common'; import { SetMetadata } from '@nestjs/common'; -import { HASURA_EVENT_HANDLER, HASURA_MODULE_CONFIG } from './hasura.constants'; -import { HasuraEventHandlerConfig } from './hasura.interfaces'; +import { + HASURA_EVENT_HANDLER, + HASURA_MODULE_CONFIG, + HASURA_SCHEDULED_EVENT_HANDLER, +} from './hasura.constants'; +import { + HasuraEventHandlerConfig, + TrackedHasuraEventHandlerConfig, + TrackedHasuraScheduledEventHandlerConfig, +} from './hasura.interfaces'; export const HasuraEventHandler = (config: HasuraEventHandlerConfig) => ( target, @@ -10,3 +18,18 @@ export const HasuraEventHandler = (config: HasuraEventHandlerConfig) => ( ) => SetMetadata(HASURA_EVENT_HANDLER, config)(target, key, descriptor); export const InjectHasuraConfig = makeInjectableDecorator(HASURA_MODULE_CONFIG); + +export const TrackedHasuraEventHandler = ( + config: TrackedHasuraEventHandlerConfig +) => SetMetadata(HASURA_EVENT_HANDLER, config); + +export const TrackedHasuraScheduledEventHandler = ( + config: TrackedHasuraScheduledEventHandlerConfig +) => (target, key, descriptor) => { + SetMetadata(HASURA_SCHEDULED_EVENT_HANDLER, config)(target, key, descriptor); + SetMetadata(HASURA_EVENT_HANDLER, { triggerName: config.name })( + target, + key, + descriptor + ); +}; diff --git a/packages/hasura/src/hasura.event-handler.guard.ts b/packages/hasura/src/hasura.event-handler.guard.ts index dc3a0ef55..986ad4123 100644 --- a/packages/hasura/src/hasura.event-handler.guard.ts +++ b/packages/hasura/src/hasura.event-handler.guard.ts @@ -12,9 +12,9 @@ export class HasuraEventHandlerHeaderGuard implements CanActivate { private readonly hasuraConfig: HasuraModuleConfig ) { this.apiSecret = - typeof hasuraConfig.secretFactory === 'function' - ? hasuraConfig.secretFactory() - : hasuraConfig.secretFactory; + typeof hasuraConfig.webhookConfig.secretFactory === 'function' + ? hasuraConfig.webhookConfig.secretFactory() + : hasuraConfig.webhookConfig.secretFactory; } canActivate( @@ -22,7 +22,8 @@ export class HasuraEventHandlerHeaderGuard implements CanActivate { ): boolean | Promise | Observable { const request = context.switchToHttp().getRequest(); - const secretRequestHeader = request.headers[this.hasuraConfig.secretHeader]; + const secretRequestHeader = + request.headers[this.hasuraConfig.webhookConfig.secretHeader]; return secretRequestHeader === this.apiSecret; } diff --git a/packages/hasura/src/hasura.interfaces.ts b/packages/hasura/src/hasura.interfaces.ts index 28aa85222..015e50fbc 100644 --- a/packages/hasura/src/hasura.interfaces.ts +++ b/packages/hasura/src/hasura.interfaces.ts @@ -16,11 +16,23 @@ export type EventOperation = 'INSERT' | 'UPDATE' | 'DELETE' | 'MANUAL'; type EventPayload = { session_variables: Record; op: EventOperation; - data: { old: unknown; new: unknown | unknown[] }; + data: { old: unknown; new: unknown }; }; -type TypedEventPayload = Omit & { - data: { old?: T; new: T | T[] }; +export type TypedEventPayload = Omit & { + data: { old?: T; new: T }; +}; + +export type InsertEventPayload = Omit & { + data: { old: null; new: T }; +}; + +export type UpdateEventPayload = Omit & { + data: { old: T; new: T }; +}; + +export type DeleteEventPayload = Omit & { + data: { old: T; new: null }; }; export type HasuraEvent = Omit & { @@ -31,17 +43,61 @@ export type TypedHasuraEvent = Omit & { event: TypedEventPayload; }; +export type HasuraInsertEvent = Omit & { + event: InsertEventPayload; +}; + +export type HasuraUpdateEvent = Omit & { + event: UpdateEventPayload; +}; + +export type HasuraDeleteEvent = Omit & { + event: DeleteEventPayload; +}; + export interface HasuraEventHandlerConfig { - /** - * @deprecated Table information for the event trigger which will will be used to route the event. - * It is recommended to use `triggerName` instead as multiple events can use the same table which - * makes routing to the correct handler more difficult - */ - table?: { schema?: string; name: string }; /** * The name of the Hasura Trigger which created this event */ - triggerName?: string; + triggerName: string; +} + +type InsertDefinition = { type: 'insert' }; +type DeleteDefinition = { type: 'delete' }; +type UpdateDefinition = { type: 'update'; columns?: string[] }; + +export interface EventRetryConfig { + numRetries: number; + timeoutInSeconds: number; + intervalInSeconds: number; +} + +export interface ScheduledEventRetryConfig extends EventRetryConfig { + toleranceSeconds: number; +} + +export interface TrackedHasuraEventHandlerConfig { + schema?: string; + tableName: string; + triggerName: string; + retryConfig?: EventRetryConfig; + definition: InsertDefinition | DeleteDefinition | UpdateDefinition; +} + +export interface TrackedHasuraScheduledEventHandlerConfig { + name: string; + cronSchedule: string; + payload: any; + comment?: string; + retryConfig?: ScheduledEventRetryConfig; +} + +export enum CommonCronSchedules { + EveryMinute = '* * * * *', + EveryTenMinutes = '*/10 * * * *', + EveryMidnight = '0 0 * * *', + EveryMonthStart = '0 0 1 * *', + EveryFridayNoon = '0 12 * * 5', } export interface HasuraScheduledEventPayload> { @@ -53,9 +109,57 @@ export interface HasuraScheduledEventPayload> { } export interface HasuraModuleConfig { - secretHeader: string; - secretFactory: (() => string) | string; + /** + * Configuration for validating webhooks from Hasura for Events and Actions + */ + webhookConfig: { + /** + * The name of the Header that Hasura will send along with all event payloads + */ + secretHeader: string; + + /** + * The value of the secret Header. The Hasura module will ensure that incoming webhook payloads contain this + * value in order to validate that it is a trusted request + */ + secretFactory: (() => string) | string; + }; + + /** + * Important: This should only be enabled for local development + * + * Including this configuration will allow Table Event triggers and Scheduled Events to be + * automatically managed inside of your Hasura metadata. This effectively makes the NestJS + * application code the source of truth for their configuration and removes a significant + * amount of boilerplate + */ + managedMetaDataConfig?: { + /** + * The ENV key in which Hasura will store the secret header value used to validate event payloads + */ + secretHeaderEnvName: string; + + /** + * The ENV key in which Hasura will store the NestJS endpoint for delivering events + */ + nestEndpointEnvName: string; + + /** + * The path to the hasura metadata directory + */ + dirPath: string; + + /** + * Default retry configuration that will be used for events that do not specify their own retry config + */ + defaultEventRetryConfig?: ScheduledEventRetryConfig; + }; enableEventLogs?: boolean; + + /** + * The default controller prefix that will be used for exposing a Webhook that can be used by Hasura + * to send events. Defaults to 'hasura' + */ controllerPrefix?: string; } diff --git a/packages/hasura/src/hasura.metadata.ts b/packages/hasura/src/hasura.metadata.ts new file mode 100644 index 000000000..e2c9d0c3f --- /dev/null +++ b/packages/hasura/src/hasura.metadata.ts @@ -0,0 +1,187 @@ +import { + HasuraEventHandlerConfig, + HasuraModuleConfig, + ScheduledEventRetryConfig, + TrackedHasuraEventHandlerConfig, + TrackedHasuraScheduledEventHandlerConfig, +} from './hasura.interfaces'; +import { safeLoad, safeDump } from 'js-yaml'; +import { readFileSync, writeFileSync } from 'fs'; +import { + TableEntry, + EventTriggerDefinition, + Columns, + CronTrigger, +} from './hasura-metadata-dist/HasuraMetadataV2'; + +const utf8 = 'utf-8'; + +const defaultHasuraRetryConfig: ScheduledEventRetryConfig = { + intervalInSeconds: 10, + numRetries: 3, + timeoutInSeconds: 60, + toleranceSeconds: 21600, +}; + +const convertEventTriggerDefinition = ( + configDef: TrackedHasuraEventHandlerConfig['definition'] +): EventTriggerDefinition => { + if (configDef.type === 'insert') { + return { + enable_manual: false, + insert: { + columns: Columns.Empty, + }, + }; + } + + if (configDef.type === 'delete') { + return { + enable_manual: false, + delete: { + columns: Columns.Empty, + }, + }; + } + + return { + enable_manual: false, + update: { + columns: configDef.columns ?? Columns.Empty, + }, + }; +}; + +export const isTrackedHasuraEventHandlerConfig = ( + eventHandlerConfig: HasuraEventHandlerConfig | TrackedHasuraEventHandlerConfig +): eventHandlerConfig is TrackedHasuraEventHandlerConfig => { + return 'definition' in eventHandlerConfig; +}; + +export const updateEventTriggerMeta = ( + moduleConfig: HasuraModuleConfig, + eventHandlerConfigs: TrackedHasuraEventHandlerConfig[] +) => { + const { managedMetaDataConfig } = moduleConfig; + + if (!managedMetaDataConfig) { + throw new Error('No configuration for meta available'); + } + + const defaultRetryConfig = + managedMetaDataConfig.defaultEventRetryConfig ?? defaultHasuraRetryConfig; + + const tablesYamlPath = `${managedMetaDataConfig.dirPath}/tables.yaml`; + + const tablesMeta = readFileSync(tablesYamlPath, utf8); + const tableEntries = safeLoad(tablesMeta) as TableEntry[]; + + eventHandlerConfigs.forEach((config) => { + const { + schema = 'public', + tableName, + triggerName, + definition, + retryConfig = defaultRetryConfig, + } = config; + const matchingTable = tableEntries.find( + (x) => x.table.schema === schema && x.table.name === tableName + ); + + if (!matchingTable) { + throw new Error( + `Table '${tableName}' from schema '${schema}' not found in tables metadata` + ); + } + + const { intervalInSeconds, numRetries, timeoutInSeconds } = retryConfig; + const eventTriggers = (matchingTable.event_triggers ?? []).filter( + (x) => x.name !== triggerName + ); + + matchingTable.event_triggers = [ + ...eventTriggers, + { + name: triggerName, + webhook_from_env: managedMetaDataConfig.nestEndpointEnvName, + headers: [ + { + name: moduleConfig.webhookConfig.secretHeader, + value_from_env: managedMetaDataConfig.secretHeaderEnvName, + }, + ], + retry_conf: { + interval_sec: intervalInSeconds, + num_retries: numRetries, + timeout_sec: timeoutInSeconds, + }, + definition: convertEventTriggerDefinition(definition), + }, + ]; + }); + + const yamlString = safeDump(tableEntries); + writeFileSync(tablesYamlPath, yamlString, utf8); +}; + +export const updateScheduledEventTriggerMeta = ( + moduleConfig: HasuraModuleConfig, + scheduledEventHandlerConfigs: TrackedHasuraScheduledEventHandlerConfig[] +) => { + const { managedMetaDataConfig } = moduleConfig; + + if (!managedMetaDataConfig) { + throw new Error('No configuration for meta available'); + } + + const cronTriggersYamlPath = `${managedMetaDataConfig.dirPath}/cron_triggers.yaml`; + + const cronTriggersMeta = readFileSync(cronTriggersYamlPath, utf8); + const cronEntries = (safeLoad(cronTriggersMeta) ?? []) as CronTrigger[]; + + const managedCronTriggerNames = scheduledEventHandlerConfigs.map( + (x) => x.name + ); + + const defaultRetryConfig = + managedMetaDataConfig.defaultEventRetryConfig ?? defaultHasuraRetryConfig; + + const managedCronTriggers: CronTrigger[] = scheduledEventHandlerConfigs.map( + ({ + name, + payload, + comment, + cronSchedule, + retryConfig = defaultRetryConfig, + }) => { + return { + name, + payload, + comment, + schedule: cronSchedule, + include_in_metadata: true, + headers: [ + { + name: moduleConfig.webhookConfig.secretHeader, + value_from_env: managedMetaDataConfig.secretHeaderEnvName, + }, + ], + webhook: `{{${managedMetaDataConfig.nestEndpointEnvName}}}`, + retry_conf: { + num_retries: retryConfig.numRetries, + retry_interval_seconds: retryConfig.intervalInSeconds, + timeout_seconds: retryConfig.timeoutInSeconds, + tolerance_seconds: retryConfig.toleranceSeconds, + }, + }; + } + ); + + const newCronEntries: CronTrigger[] = [ + ...cronEntries.filter((x) => !managedCronTriggerNames.includes(x.name)), + ...managedCronTriggers, + ]; + + const yamlString = safeDump(newCronEntries); + writeFileSync(cronTriggersYamlPath, yamlString, utf8); +}; diff --git a/packages/hasura/src/hasura.module.ts b/packages/hasura/src/hasura.module.ts index 445e38420..67f59c325 100644 --- a/packages/hasura/src/hasura.module.ts +++ b/packages/hasura/src/hasura.module.ts @@ -9,7 +9,11 @@ import { import { PATH_METADATA } from '@nestjs/common/constants'; import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator'; import { flatten, groupBy } from 'lodash'; -import { HASURA_EVENT_HANDLER, HASURA_MODULE_CONFIG } from './hasura.constants'; +import { + HASURA_EVENT_HANDLER, + HASURA_MODULE_CONFIG, + HASURA_SCHEDULED_EVENT_HANDLER, +} from './hasura.constants'; import { InjectHasuraConfig } from './hasura.decorators'; import { EventHandlerController } from './hasura.event-handler.controller'; import { HasuraEventHandlerHeaderGuard } from './hasura.event-handler.guard'; @@ -19,7 +23,14 @@ import { HasuraEventHandlerConfig, HasuraModuleConfig, HasuraScheduledEventPayload, + TrackedHasuraEventHandlerConfig, + TrackedHasuraScheduledEventHandlerConfig, } from './hasura.interfaces'; +import { + isTrackedHasuraEventHandlerConfig, + updateEventTriggerMeta, + updateScheduledEventTriggerMeta, +} from './hasura.metadata'; function isHasuraEvent(value: any): value is HasuraEvent { return ['trigger', 'table', 'event'].every((it) => it in value); @@ -70,6 +81,7 @@ export class HasuraModule super(); } + // eslint-disable-next-line sonarjs/cognitive-complexity public async onModuleInit() { this.logger.log('Initializing Hasura Module'); @@ -77,6 +89,43 @@ export class HasuraModule HasuraEventHandlerConfig >(HASURA_EVENT_HANDLER); + const trackedEventHandlerMeta = await this.discover.providerMethodsWithMetaAtKey< + HasuraEventHandlerConfig | TrackedHasuraEventHandlerConfig + >(HASURA_EVENT_HANDLER); + + const trackedScheduledEventHandlerMeta = await this.discover.providerMethodsWithMetaAtKey< + TrackedHasuraScheduledEventHandlerConfig + >(HASURA_SCHEDULED_EVENT_HANDLER); + + if (!eventHandlerMeta.length) { + this.logger.log('No Hasura event handlers were discovered'); + return; + } + + this.logger.log( + `Discovered ${eventHandlerMeta.length} hasura event handlers` + ); + + if (this.hasuraModuleConfig.managedMetaDataConfig) { + this.logger.log( + 'Automatically syncing hasura metadata based on discovered event handlers. Remember to apply any changes to your Hasura instance using the CLI' + ); + + updateEventTriggerMeta( + this.hasuraModuleConfig, + trackedEventHandlerMeta + .filter((x) => isTrackedHasuraEventHandlerConfig(x.meta)) + .map((x) => x.meta as TrackedHasuraEventHandlerConfig) + ); + + if (trackedScheduledEventHandlerMeta.length) { + updateScheduledEventTriggerMeta( + this.hasuraModuleConfig, + trackedScheduledEventHandlerMeta.map((x) => x.meta) + ); + } + } + const grouped = groupBy( eventHandlerMeta, (x) => x.discoveredMethod.parentClass.name @@ -87,32 +136,8 @@ export class HasuraModule this.logger.log(`Registering hasura event handlers from ${x}`); return grouped[x].map(({ discoveredMethod, meta: config }) => { - if (!config.table && !config.triggerName) { - throw new Error( - 'Hasura Event Handler is invalid. Specify either trigger name or table mapping' - ); - } - - if (config.table) { - this.logger.warn( - `Event binding based on schema and table is deprecated and will be removed in a future release. Consider replacing the binding on ${discoveredMethod.methodName} with triggerName` - ); - } - - if (config.table && config.triggerName) { - this.logger.warn( - `Both table and trigger bindings are set for ${discoveredMethod.methodName}. This is not recommended and will cause duplicate message processing` - ); - } - - const key = - config.triggerName || - `${config.table?.schema ? config.table?.schema : 'public'}-${ - config.table?.name - }`; - return { - key, + key: config.triggerName, handler: this.externalContextCreator.create( discoveredMethod.parentClass.instance, discoveredMethod.handler, diff --git a/packages/hasura/src/tests/hasura.module.spec.ts b/packages/hasura/src/tests/hasura.module.spec.ts index 3bbab3ffd..5fe091c4d 100644 --- a/packages/hasura/src/tests/hasura.module.spec.ts +++ b/packages/hasura/src/tests/hasura.module.spec.ts @@ -9,7 +9,6 @@ import { } from '../hasura.interfaces'; import { pick } from 'lodash'; -const tableBoundEventHandler = jest.fn(); const triggerBoundEventHandler = jest.fn(); const scheduledEventHandler = jest.fn(); const triggerName = 'user_created'; @@ -18,13 +17,6 @@ const defaultHasuraEndpoint = '/hasura/events'; @Injectable() class UserEventService { - @HasuraEventHandler({ - table: { name: 'user' }, - }) - handleUserTableEvent(evt) { - tableBoundEventHandler(evt); - } - @HasuraEventHandler({ triggerName, }) @@ -87,8 +79,10 @@ describe.each(cases)( : defaultHasuraEndpoint; const moduleConfig: HasuraModuleConfig = { - secretFactory: secret, - secretHeader: secretHeader, + webhookConfig: { + secretFactory: secret, + secretHeader: secretHeader, + }, controllerPrefix, enableEventLogs: true, }; @@ -111,7 +105,6 @@ describe.each(cases)( }); afterEach(() => { - tableBoundEventHandler.mockReset(); triggerBoundEventHandler.mockReset(); scheduledEventHandler.mockReset(); }); @@ -146,8 +139,6 @@ describe.each(cases)( .send(eventPayload); expect(response.status).toEqual(202); - expect(tableBoundEventHandler).toHaveBeenCalledTimes(1); - expect(tableBoundEventHandler).toHaveBeenCalledWith(eventPayload); expect(triggerBoundEventHandler).toHaveBeenCalledTimes(1); expect(triggerBoundEventHandler).toHaveBeenCalledWith(eventPayload); }); diff --git a/packages/hasura/tsconfig.json b/packages/hasura/tsconfig.json index e748f35c6..6948378de 100644 --- a/packages/hasura/tsconfig.json +++ b/packages/hasura/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./lib", - "rootDir": "./src" + "rootDir": "./src", + "skipLibCheck": true, + "allowJs": true }, "include": ["./src"], "references": [{ "path": "../discovery" }, { "path": "../modules" }] diff --git a/yarn.lock b/yarn.lock index a8253dbfe..dad63057f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -368,6 +368,11 @@ unique-filename "^1.1.1" which "^1.3.1" +"@hasura/metadata@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@hasura/metadata/-/metadata-1.0.2.tgz#0e212a349a176108c1f2572faa03317d21c2f052" + integrity sha512-bVDwRWC7g/NfLVUwP8HBV07+37g07UAbF+XEujfRmgr8839sH7Q2iwa2M8oQFQXwg4dj5Sn+WRt4/UWXKN7naQ== + "@iamstarkov/listr-update-renderer@0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e" @@ -1545,6 +1550,11 @@ dependencies: "@types/jest-diff" "*" +"@types/js-yaml@^3.12.5": + version "3.12.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" + integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== + "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -5502,6 +5512,14 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"