Last active
July 18, 2025 22:00
-
-
Save jsmnbom/f5305384126b8ac02ad0977c9b960d71 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Plugin(s) to add typedPgRegistry to the build input. | |
| * TypedPgRegistryPlugin adds the typedPgRegistry to the build input, and should always be used. | |
| * ExportPgRegistryTypesPlugin exports types for the PgRegistry, and should only be used in development. | |
| * The exported .d.ts file should be added to the tsconfig.json `include` array. | |
| * The plugin only exports if the `exportPgRegistryTypesPath` option is set in the schema options. | |
| */ | |
| import type { PgCodec, PgEnumCodec, PgResource } from '@dataplan/pg' | |
| import { mkdir, readFile, writeFile } from 'node:fs/promises' | |
| import { dirname } from 'node:path' | |
| const version = '0.5.0' | |
| declare global { | |
| // eslint-disable-next-line ts/no-namespace | |
| namespace GraphileBuild { | |
| interface SchemaOptions { | |
| exportPgRegistryTypesPath?: string | |
| } | |
| } | |
| } | |
| async function writeFileIfDiffers( | |
| path: string, | |
| contents: string, | |
| ): Promise<void> { | |
| await mkdir(dirname(path), { recursive: true }) | |
| const oldContents = await readFile(path, 'utf8').catch(() => null) | |
| if (oldContents !== contents) { | |
| await writeFile(path, contents) | |
| } | |
| } | |
| export const TypedPgRegistryPlugin: GraphileConfig.Plugin = { | |
| name: 'TypedPgRegistryPlugin', | |
| description: | |
| 'Adds typedPgRegistry to the build input.', | |
| version, | |
| after: ['PgRegistryPlugin'], | |
| gather: { | |
| async main(output) { | |
| (output as unknown as { | |
| typedPgRegistry: typeof output.pgRegistry | |
| }).typedPgRegistry = output.pgRegistry | |
| }, | |
| }, | |
| } | |
| interface $$Export { | |
| moduleName: string | |
| exportName: string | 'default' | '*' | string[] | |
| } | |
| interface Importable { | |
| $$export: $$Export | |
| } | |
| interface AnyFunction { | |
| (...args: any[]): any | |
| displayName?: string | |
| } | |
| function isImportable( | |
| thing: unknown, | |
| ): thing is { $$export: $$Export } { | |
| return ( | |
| (typeof thing === 'object' || typeof thing === 'function') | |
| && thing !== null | |
| && '$$export' in (thing as object | AnyFunction) | |
| ) | |
| } | |
| function isEnumCodec(codec: PgCodec): codec is PgEnumCodec { | |
| return codec.isEnum === true | |
| } | |
| export const ExportPgRegistryTypesPlugin: GraphileConfig.Plugin = { | |
| name: 'ExportPgRegistryTypesPlugin', | |
| description: | |
| 'Exports types for the PgRegistry. Should only be used in development.', | |
| version, | |
| schema: { | |
| hooks: { | |
| finalize(schema, build) { | |
| const { exportPgRegistryTypesPath } = build.options | |
| const { pgExecutors, pgResources, pgCodecs, pgRelations } = build.input.pgRegistry | |
| if (!exportPgRegistryTypesPath) | |
| return schema | |
| const codecTypeName = (codec: PgCodec) => `${codec.name}Codec`.replace(/\W/g, '_') | |
| const resourceTypeName = (resource: PgResource) => `${resource.name}Resource`.replace(/\W/g, '_') | |
| const namedImports: Record<string, Set<string>> = {} | |
| const starImports = new Set<string>() | |
| const defaultImports = new Set<string>() | |
| function addNamedImport(moduleName: string, name: string): void { | |
| if (!namedImports[moduleName]) | |
| namedImports[moduleName] = new Set() | |
| namedImports[moduleName].add(name) | |
| } | |
| function asImported(thing: Importable): string { | |
| if (thing.$$export.exportName === 'default') { | |
| defaultImports.add(thing.$$export.moduleName) | |
| return thing.$$export.moduleName | |
| } | |
| else if (thing.$$export.exportName === '*') { | |
| starImports.add(thing.$$export.moduleName) | |
| return thing.$$export.moduleName | |
| } | |
| else if (Array.isArray(thing.$$export.exportName)) { | |
| addNamedImport(thing.$$export.moduleName, thing.$$export.exportName[0]!) | |
| return thing.$$export.exportName.join('.') | |
| } | |
| addNamedImport(thing.$$export.moduleName, thing.$$export.exportName) | |
| return thing.$$export.exportName | |
| } | |
| const codecs = Object.values(pgCodecs).map((codec) => { | |
| if (isImportable(codec)) { | |
| return `export type ${codecTypeName(codec)} = typeof ${asImported(codec)};` | |
| } | |
| const TName = codec.name | |
| if (isEnumCodec(codec)) { | |
| return `export type ${codecTypeName(codec)} = PgEnumCodec<'${TName}', ${codec.values.map(value => `'${value.value}'`).join(' | ')}>;` | |
| } | |
| if (codec.attributes) { | |
| const TAttributes = [ | |
| '{', | |
| indent( | |
| Object.entries(codec.attributes) | |
| .map(([key, attribute]) => `${key}: PgCodecAttribute<${codecTypeName(attribute.codec)}, ${attribute.notNull ? 'true' : 'false'}>`) | |
| .join('\n'), | |
| ), | |
| '}', | |
| ].join('\n') | |
| return `export type ${codecTypeName(codec)} = PgRecordCodec<'${TName}', ${TAttributes}>;` | |
| } | |
| let TFromPostgres = 'any' | |
| let TFromJavaScript = 'any' | |
| let TArrayItemCodec = 'undefined' | |
| let TDomainItemCodec = 'undefined' | |
| let TRangeItemCodec = 'undefined' | |
| if (codec.domainOfCodec) { | |
| TFromPostgres = `readonly InferPgCodecTFromPostgres<${codecTypeName(codec.domainOfCodec)}>` | |
| TFromJavaScript = `readonly InferPgCodecTFromJavaScript<${codecTypeName(codec.domainOfCodec)}>[]` | |
| TDomainItemCodec = codecTypeName(codec.domainOfCodec) | |
| } | |
| else if (codec.rangeOfCodec) { | |
| TFromPostgres = 'string' | |
| TFromJavaScript = `PgRange<unknown>` | |
| TRangeItemCodec = codecTypeName(codec.rangeOfCodec) | |
| } | |
| else if (codec.arrayOfCodec) { | |
| TFromPostgres = 'string' | |
| TFromJavaScript = `readonly InferPgCodecTFromJavaScript<${codecTypeName(codec.arrayOfCodec)}>[]` | |
| TArrayItemCodec = codecTypeName(codec.arrayOfCodec) | |
| } | |
| return `export type ${codecTypeName(codec)} = PgCodec<'${TName}', undefined, ${TFromPostgres}, ${TFromJavaScript}, ${TArrayItemCodec}, ${TDomainItemCodec}, ${TRangeItemCodec}>;` | |
| }) | |
| const resources = Object.values(pgResources).map((resource) => { | |
| if (isImportable(resource)) { | |
| return `export type ${resourceTypeName(resource)} = typeof ${asImported(resource)};` | |
| } | |
| const TName = resource.name | |
| const TCodec = `${codecTypeName(resource.codec)}` | |
| const TUniques = resource.uniques && resource.uniques.length > 0 | |
| ? [ | |
| 'readonly [', | |
| indent( | |
| resource.uniques | |
| .map(unique => `{ attributes: readonly [${unique.attributes.map(attr => `'${attr}'`).join(', ')}]}`) | |
| .join(',\n'), | |
| ), | |
| ']', | |
| ].join('\n') | |
| : 'readonly []' | |
| const TParameters = resource.parameters && resource.parameters.length > 0 | |
| ? [ | |
| 'readonly [', | |
| indent( | |
| resource.parameters | |
| .map(parameter => `PgResourceParameter<'${parameter.name}', ${codecTypeName(parameter.codec)}>`) | |
| .join(',\n'), | |
| ), | |
| ']', | |
| ].join('\n') | |
| : 'undefined' | |
| return `export type ${resource.name}Resource = PgResourceOptions<'${TName}', ${TCodec}, ${TUniques}, ${TParameters}>;` | |
| }) | |
| const relations = Object.fromEntries(Object.entries(pgRelations).map(([codecName, relations]) => [codecName, Object.fromEntries( | |
| Object.entries(relations).map(([relationName, relation]) => [ | |
| relationName, | |
| [ | |
| '{', | |
| indent([ | |
| `localCodec: ${codecTypeName(relation.localCodec)}`, | |
| `remoteResourceOptions: ${resourceTypeName(relation.remoteResource)}`, | |
| `localAttributes: readonly [${relation.localAttributes.map(attr => `'${attr}'`).join(', ')}]`, | |
| `remoteAttributes: readonly [${relation.remoteAttributes.map(attr => `'${attr}'`).join(', ')}]`, | |
| `isUnique: ${relation.isUnique ? 'true' : 'false'}`, | |
| ].join('\n')), | |
| '}', | |
| ].join('\n'), | |
| ]), | |
| )])) | |
| const code = `/* | |
| * This file is auto-generated by the PgRegistryTypesPlugin. | |
| * Do not edit this file directly. | |
| */ | |
| import type { PgCodec, PgCodecAttribute, PgExecutor, PgRegistry, PgResourceOptions, PgResourceParameter, ObjectFromPgCodecAttributes } from '@dataplan/pg'; | |
| ${[ | |
| ...Object.entries(namedImports).map(([moduleName, names]) => `import type { ${[...names].join(', ')} } from '${moduleName}';`), | |
| ...[...starImports].map(moduleName => `import * as ${moduleName} from '${moduleName}';`), | |
| ...[...defaultImports].map(moduleName => `import ${moduleName} from '${moduleName}';`), | |
| ].join('\n')} | |
| type InferPgCodecTFromJavaScript<TCodec> = | |
| TCodec extends PgCodec< | |
| infer _Name, | |
| infer _Attrs, | |
| infer FromPg, | |
| infer _FromJs, | |
| infer _Array, | |
| infer _Domain, | |
| infer _Range | |
| > ? FromPg : never; | |
| type InferPgCodecTFromJavaScript<TCodec> = | |
| TCodec extends PgCodec< | |
| infer _Name, | |
| infer _Attrs, | |
| infer _FromPg, | |
| infer FromJs, | |
| infer _Array, | |
| infer _Domain, | |
| infer _Range | |
| > ? FromJs : never; | |
| type PgRecordCodec<TName extends string, const TAttributes extends PgCodecAttributes> = | |
| PgCodec<TName, TAttributes, string, ObjectFromPgCodecAttributes<TAttributes>, undefined, undefined, undefined>; | |
| ${codecs.join('\n')} | |
| export type Codecs = { | |
| ${Object.entries(pgCodecs) | |
| .map(([name, codec]) => `'${name}': ${codecTypeName(codec)};`) | |
| .join('\n ')} | |
| }; | |
| ${resources.join('\n')} | |
| export type ResourceOptions = { | |
| ${Object.entries(pgResources) | |
| .map(([name, resource]) => `'${name}': ${resourceTypeName(resource)};`) | |
| .join('\n ')} | |
| }; | |
| export type Relations = { | |
| ${indent(Object.entries(relations) | |
| .map(([codecName, codecRelations]) => `'${codecName}': { \n${indent( | |
| Object.entries(codecRelations) | |
| .map(([relationName, relationType]) => `'${relationName}': ${relationType};`) | |
| .join('\n'), | |
| )} \n};`) | |
| .join('\n')) | |
| } | |
| }; | |
| export type Executors = { | |
| ${Object.entries(pgExecutors) | |
| .map(([name, executor]) => `'${name}': PgExecutor<'${executor.name}'>;`) | |
| .join('\n ')} | |
| } | |
| export type TypedPgRegistry = PgRegistry< | |
| Codecs, | |
| ResourceOptions, | |
| Relations, | |
| Executors | |
| >; | |
| declare global { | |
| namespace GraphileBuild { | |
| interface BuildInput { | |
| typedPgRegistry: TypedPgRegistry | |
| } | |
| } | |
| } | |
| ` | |
| writeFileIfDiffers(exportPgRegistryTypesPath, code).catch((e) => { | |
| console.error( | |
| `Failed to write PgRegistry types to '${exportPgRegistryTypesPath}': ${e}`, | |
| ) | |
| }) | |
| return schema | |
| }, | |
| }, | |
| }, | |
| } | |
| function indent( | |
| lines: string, | |
| spaces: number = 2, | |
| ): string { | |
| const indentString = ' '.repeat(spaces) | |
| return lines.replace(/^(?!\s*$)/gm, indentString) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment