@stonecrop/schema 0.8.7 → 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cli.cjs +1 -0
  2. package/dist/cli.cjs.map +1 -0
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/converter/heuristics.js +254 -0
  6. package/dist/converter/index.js +164 -0
  7. package/dist/converter/scalars.js +86 -0
  8. package/dist/converter/types.js +5 -0
  9. package/dist/doctype.js +52 -0
  10. package/dist/field.js +82 -0
  11. package/dist/fieldtype.js +70 -0
  12. package/dist/index-COrltkHl.js +1 -0
  13. package/dist/index-COrltkHl.js.map +1 -0
  14. package/dist/index-aeXXzPET.cjs +1 -0
  15. package/dist/index-aeXXzPET.cjs.map +1 -0
  16. package/dist/index.cjs +1 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/naming.js +106 -0
  21. package/dist/{index.d.ts → schema.d.ts} +10 -2
  22. package/dist/schema.tsbuildinfo +1 -0
  23. package/dist/src/cli.d.ts +15 -0
  24. package/dist/src/cli.d.ts.map +1 -0
  25. package/dist/src/converter/heuristics.d.ts +60 -0
  26. package/dist/src/converter/heuristics.d.ts.map +1 -0
  27. package/dist/src/converter/index.d.ts +47 -0
  28. package/dist/src/converter/index.d.ts.map +1 -0
  29. package/dist/src/converter/scalars.d.ts +46 -0
  30. package/dist/src/converter/scalars.d.ts.map +1 -0
  31. package/dist/src/converter/types.d.ts +145 -0
  32. package/dist/src/converter/types.d.ts.map +1 -0
  33. package/dist/src/doctype.d.ts +312 -0
  34. package/dist/src/doctype.d.ts.map +1 -0
  35. package/dist/src/field.d.ts +137 -0
  36. package/dist/src/field.d.ts.map +1 -0
  37. package/dist/src/fieldtype.d.ts +41 -0
  38. package/dist/src/fieldtype.d.ts.map +1 -0
  39. package/dist/src/index.d.ts +11 -0
  40. package/dist/src/index.d.ts.map +1 -0
  41. package/dist/src/naming.d.ts +80 -0
  42. package/dist/src/naming.d.ts.map +1 -0
  43. package/dist/src/tsdoc-metadata.json +11 -0
  44. package/dist/src/validation.d.ts +55 -0
  45. package/dist/src/validation.d.ts.map +1 -0
  46. package/dist/validation.js +60 -0
  47. package/package.json +5 -5
  48. package/dist/cli.d.ts +0 -1
package/dist/cli.cjs CHANGED
@@ -38,3 +38,4 @@ EXAMPLES:
38
38
  stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\
39
39
  --include "User,Post,Comment"
40
40
  `)}$().catch(e=>{console.error("Error:",e.message),process.exit(1)});
41
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.cjs","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Stonecrop Schema CLI\n *\n * Converts GraphQL introspection results to Stonecrop doctype JSON schemas.\n *\n * Usage:\n * stonecrop-schema generate --endpoint <url> --output <dir>\n * stonecrop-schema generate --introspection <file.json> --output <dir>\n * stonecrop-schema generate --sdl <file.graphql> --output <dir>\n *\n * @packageDocumentation\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { getIntrospectionQuery, type IntrospectionQuery } from 'graphql'\n\nimport { convertGraphQLSchema } from './converter/index'\nimport { validateDoctype } from './validation'\nimport type { GraphQLConversionOptions } from './converter/types'\n\n/**\n * Fetch an introspection result from a live GraphQL endpoint.\n *\n * @param endpoint - The GraphQL endpoint URL\n * @param headers - Optional HTTP headers\n * @returns The introspection query result\n */\nasync function fetchIntrospection(endpoint: string, headers?: Record<string, string>): Promise<IntrospectionQuery> {\n\tconst response = await fetch(endpoint, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...headers,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tquery: getIntrospectionQuery(),\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`)\n\t}\n\n\tconst json = (await response.json()) as {\n\t\tdata?: IntrospectionQuery\n\t\terrors?: Array<{ message: string }>\n\t}\n\n\tif (json.errors?.length) {\n\t\tthrow new Error(`GraphQL errors: ${json.errors.map(e => e.message).join(', ')}`)\n\t}\n\n\tif (!json.data) {\n\t\tthrow new Error('No data in introspection response')\n\t}\n\n\treturn json.data\n}\n\nasync function main(): Promise<void> {\n\tconst { values, positionals } = parseArgs({\n\t\tallowPositionals: true,\n\t\toptions: {\n\t\t\tendpoint: { type: 'string', short: 'e' },\n\t\t\tintrospection: { type: 'string', short: 'i' },\n\t\t\tsdl: { type: 'string', short: 's' },\n\t\t\toutput: { type: 'string', short: 'o' },\n\t\t\tinclude: { type: 'string' },\n\t\t\texclude: { type: 'string' },\n\t\t\toverrides: { type: 'string' },\n\t\t\t'custom-scalars': { type: 'string' },\n\t\t\t'include-unmapped': { type: 'boolean', default: false },\n\t\t\thelp: { type: 'boolean', short: 'h' },\n\t\t},\n\t})\n\n\tconst command = positionals[0]\n\n\tif (values.help || !command) {\n\t\tprintHelp()\n\t\tprocess.exit(command ? 0 : 1)\n\t}\n\n\tif (command !== 'generate') {\n\t\tconsole.error(`Unknown command: ${command}`)\n\t\tconsole.error('Available commands: generate')\n\t\tprocess.exit(1)\n\t}\n\n\t// Determine source\n\tconst sourceCount = [values.endpoint, values.introspection, values.sdl].filter(Boolean).length\n\tif (sourceCount !== 1) {\n\t\tconsole.error('Exactly one of --endpoint, --introspection, or --sdl must be provided')\n\t\tprocess.exit(1)\n\t}\n\n\tif (!values.output) {\n\t\tconsole.error('--output <dir> is required')\n\t\tprocess.exit(1)\n\t}\n\n\tconst outputDir = resolve(values.output)\n\n\t// Build conversion options\n\tconst options: GraphQLConversionOptions = {\n\t\tincludeUnmappedMeta: values['include-unmapped'],\n\t}\n\n\tif (values.include) {\n\t\toptions.include = values.include.split(',').map(s => s.trim())\n\t}\n\n\tif (values.exclude) {\n\t\toptions.exclude = values.exclude.split(',').map(s => s.trim())\n\t}\n\n\tif (values.overrides) {\n\t\tconst overridesPath = resolve(values.overrides)\n\t\tconst overridesContent = readFileSync(overridesPath, 'utf-8')\n\t\toptions.typeOverrides = JSON.parse(overridesContent)\n\t}\n\n\tif (values['custom-scalars']) {\n\t\tconst scalarsPath = resolve(values['custom-scalars'])\n\t\tconst scalarsContent = readFileSync(scalarsPath, 'utf-8')\n\t\toptions.customScalars = JSON.parse(scalarsContent)\n\t}\n\n\t// Resolve source\n\tlet source: IntrospectionQuery | string\n\n\tif (values.endpoint) {\n\t\tconsole.log(`Fetching introspection from ${values.endpoint}...`)\n\t\tsource = await fetchIntrospection(values.endpoint)\n\t} else if (values.introspection) {\n\t\tconst filePath = resolve(values.introspection)\n\t\tconst content = readFileSync(filePath, 'utf-8')\n\t\tconst parsed = JSON.parse(content)\n\t\t// Handle both { data: { __schema: ... } } and { __schema: ... } formats\n\t\tsource = parsed.data ?? parsed\n\t} else {\n\t\tconst filePath = resolve(values.sdl!)\n\t\tsource = readFileSync(filePath, 'utf-8')\n\t}\n\n\t// Convert\n\tconst doctypes = convertGraphQLSchema(source, options)\n\n\tif (doctypes.length === 0) {\n\t\tconsole.warn('No entity types found in the schema. Check your include/exclude filters.')\n\t\tprocess.exit(0)\n\t}\n\n\t// Write output\n\tif (!existsSync(outputDir)) {\n\t\tmkdirSync(outputDir, { recursive: true })\n\t}\n\n\tlet warnings = 0\n\tlet errors = 0\n\n\tfor (const doctype of doctypes) {\n\t\tconst fileName = `${doctype.slug}.json`\n\t\tconst filePath = join(outputDir, fileName)\n\t\tconst json = JSON.stringify(doctype, null, '\\t')\n\n\t\twriteFileSync(filePath, json + '\\n', 'utf-8')\n\n\t\t// Validate the output\n\t\tconst validation = validateDoctype(doctype)\n\t\tif (!validation.success) {\n\t\t\terrors++\n\t\t\tconsole.error(` ERROR: ${fileName} failed validation:`)\n\t\t\tfor (const err of validation.errors) {\n\t\t\t\tconsole.error(` ${err.path.join('.')}: ${err.message}`)\n\t\t\t}\n\t\t} else {\n\t\t\t// Check for unmapped fields\n\t\t\tconst unmappedFields = doctype.fields.filter((f: any) => f._unmapped)\n\t\t\tif (unmappedFields.length > 0) {\n\t\t\t\twarnings++\n\t\t\t\tconsole.warn(\n\t\t\t\t\t` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields\n\t\t\t\t\t\t.map((f: any) => f.fieldname)\n\t\t\t\t\t\t.join(', ')}`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tconsole.log(\n\t\t`\\nGenerated ${doctypes.length} doctype(s) in ${outputDir}` +\n\t\t\t(warnings ? ` (${warnings} with warnings)` : '') +\n\t\t\t(errors ? ` (${errors} with errors)` : '')\n\t)\n\n\tif (errors > 0) {\n\t\tprocess.exit(1)\n\t}\n}\n\nfunction printHelp(): void {\n\tconsole.log(`\nstonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes\n\nUSAGE:\n stonecrop-schema generate [options]\n\nSOURCE (exactly one required):\n --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint\n --introspection, -i <file> Read from a saved introspection JSON file\n --sdl, -s <file> Read from a GraphQL SDL (.graphql) file\n\nOUTPUT:\n --output, -o <dir> Directory to write doctype JSON files (required)\n\nOPTIONS:\n --include <types> Comma-separated list of type names to include\n --exclude <types> Comma-separated list of type names to exclude\n --overrides <file> JSON file with per-type field overrides\n --custom-scalars <file> JSON file mapping custom scalar names to field templates\n --include-unmapped Include _graphqlType metadata on unmapped fields\n --help, -h Show this help message\n\nEXAMPLES:\n # From a live PostGraphile server\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas\n\n # From a saved introspection result\n stonecrop-schema generate -i introspection.json -o ./schemas\n\n # From an SDL file with custom scalars\n stonecrop-schema generate -s schema.graphql -o ./schemas \\\\\n --custom-scalars custom-scalars.json\n\n # Only convert specific types\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\\\\n --include \"User,Post,Comment\"\n`)\n}\n\nmain().catch(err => {\n\tconsole.error('Error:', err.message)\n\tprocess.exit(1)\n})\n"],"names":["fetchIntrospection","endpoint","headers","response","getIntrospectionQuery","json","e","main","values","positionals","parseArgs","command","printHelp","outputDir","resolve","options","s","overridesPath","overridesContent","readFileSync","scalarsPath","scalarsContent","source","filePath","content","parsed","doctypes","convertGraphQLSchema","existsSync","mkdirSync","warnings","errors","doctype","fileName","join","writeFileSync","validation","validateDoctype","unmappedFields","f","err"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -128,3 +128,4 @@ EXAMPLES:
128
128
  E().catch((e) => {
129
129
  console.error("Error:", e.message), process.exit(1);
130
130
  });
131
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Stonecrop Schema CLI\n *\n * Converts GraphQL introspection results to Stonecrop doctype JSON schemas.\n *\n * Usage:\n * stonecrop-schema generate --endpoint <url> --output <dir>\n * stonecrop-schema generate --introspection <file.json> --output <dir>\n * stonecrop-schema generate --sdl <file.graphql> --output <dir>\n *\n * @packageDocumentation\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { getIntrospectionQuery, type IntrospectionQuery } from 'graphql'\n\nimport { convertGraphQLSchema } from './converter/index'\nimport { validateDoctype } from './validation'\nimport type { GraphQLConversionOptions } from './converter/types'\n\n/**\n * Fetch an introspection result from a live GraphQL endpoint.\n *\n * @param endpoint - The GraphQL endpoint URL\n * @param headers - Optional HTTP headers\n * @returns The introspection query result\n */\nasync function fetchIntrospection(endpoint: string, headers?: Record<string, string>): Promise<IntrospectionQuery> {\n\tconst response = await fetch(endpoint, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...headers,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tquery: getIntrospectionQuery(),\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`)\n\t}\n\n\tconst json = (await response.json()) as {\n\t\tdata?: IntrospectionQuery\n\t\terrors?: Array<{ message: string }>\n\t}\n\n\tif (json.errors?.length) {\n\t\tthrow new Error(`GraphQL errors: ${json.errors.map(e => e.message).join(', ')}`)\n\t}\n\n\tif (!json.data) {\n\t\tthrow new Error('No data in introspection response')\n\t}\n\n\treturn json.data\n}\n\nasync function main(): Promise<void> {\n\tconst { values, positionals } = parseArgs({\n\t\tallowPositionals: true,\n\t\toptions: {\n\t\t\tendpoint: { type: 'string', short: 'e' },\n\t\t\tintrospection: { type: 'string', short: 'i' },\n\t\t\tsdl: { type: 'string', short: 's' },\n\t\t\toutput: { type: 'string', short: 'o' },\n\t\t\tinclude: { type: 'string' },\n\t\t\texclude: { type: 'string' },\n\t\t\toverrides: { type: 'string' },\n\t\t\t'custom-scalars': { type: 'string' },\n\t\t\t'include-unmapped': { type: 'boolean', default: false },\n\t\t\thelp: { type: 'boolean', short: 'h' },\n\t\t},\n\t})\n\n\tconst command = positionals[0]\n\n\tif (values.help || !command) {\n\t\tprintHelp()\n\t\tprocess.exit(command ? 0 : 1)\n\t}\n\n\tif (command !== 'generate') {\n\t\tconsole.error(`Unknown command: ${command}`)\n\t\tconsole.error('Available commands: generate')\n\t\tprocess.exit(1)\n\t}\n\n\t// Determine source\n\tconst sourceCount = [values.endpoint, values.introspection, values.sdl].filter(Boolean).length\n\tif (sourceCount !== 1) {\n\t\tconsole.error('Exactly one of --endpoint, --introspection, or --sdl must be provided')\n\t\tprocess.exit(1)\n\t}\n\n\tif (!values.output) {\n\t\tconsole.error('--output <dir> is required')\n\t\tprocess.exit(1)\n\t}\n\n\tconst outputDir = resolve(values.output)\n\n\t// Build conversion options\n\tconst options: GraphQLConversionOptions = {\n\t\tincludeUnmappedMeta: values['include-unmapped'],\n\t}\n\n\tif (values.include) {\n\t\toptions.include = values.include.split(',').map(s => s.trim())\n\t}\n\n\tif (values.exclude) {\n\t\toptions.exclude = values.exclude.split(',').map(s => s.trim())\n\t}\n\n\tif (values.overrides) {\n\t\tconst overridesPath = resolve(values.overrides)\n\t\tconst overridesContent = readFileSync(overridesPath, 'utf-8')\n\t\toptions.typeOverrides = JSON.parse(overridesContent)\n\t}\n\n\tif (values['custom-scalars']) {\n\t\tconst scalarsPath = resolve(values['custom-scalars'])\n\t\tconst scalarsContent = readFileSync(scalarsPath, 'utf-8')\n\t\toptions.customScalars = JSON.parse(scalarsContent)\n\t}\n\n\t// Resolve source\n\tlet source: IntrospectionQuery | string\n\n\tif (values.endpoint) {\n\t\tconsole.log(`Fetching introspection from ${values.endpoint}...`)\n\t\tsource = await fetchIntrospection(values.endpoint)\n\t} else if (values.introspection) {\n\t\tconst filePath = resolve(values.introspection)\n\t\tconst content = readFileSync(filePath, 'utf-8')\n\t\tconst parsed = JSON.parse(content)\n\t\t// Handle both { data: { __schema: ... } } and { __schema: ... } formats\n\t\tsource = parsed.data ?? parsed\n\t} else {\n\t\tconst filePath = resolve(values.sdl!)\n\t\tsource = readFileSync(filePath, 'utf-8')\n\t}\n\n\t// Convert\n\tconst doctypes = convertGraphQLSchema(source, options)\n\n\tif (doctypes.length === 0) {\n\t\tconsole.warn('No entity types found in the schema. Check your include/exclude filters.')\n\t\tprocess.exit(0)\n\t}\n\n\t// Write output\n\tif (!existsSync(outputDir)) {\n\t\tmkdirSync(outputDir, { recursive: true })\n\t}\n\n\tlet warnings = 0\n\tlet errors = 0\n\n\tfor (const doctype of doctypes) {\n\t\tconst fileName = `${doctype.slug}.json`\n\t\tconst filePath = join(outputDir, fileName)\n\t\tconst json = JSON.stringify(doctype, null, '\\t')\n\n\t\twriteFileSync(filePath, json + '\\n', 'utf-8')\n\n\t\t// Validate the output\n\t\tconst validation = validateDoctype(doctype)\n\t\tif (!validation.success) {\n\t\t\terrors++\n\t\t\tconsole.error(` ERROR: ${fileName} failed validation:`)\n\t\t\tfor (const err of validation.errors) {\n\t\t\t\tconsole.error(` ${err.path.join('.')}: ${err.message}`)\n\t\t\t}\n\t\t} else {\n\t\t\t// Check for unmapped fields\n\t\t\tconst unmappedFields = doctype.fields.filter((f: any) => f._unmapped)\n\t\t\tif (unmappedFields.length > 0) {\n\t\t\t\twarnings++\n\t\t\t\tconsole.warn(\n\t\t\t\t\t` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields\n\t\t\t\t\t\t.map((f: any) => f.fieldname)\n\t\t\t\t\t\t.join(', ')}`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tconsole.log(\n\t\t`\\nGenerated ${doctypes.length} doctype(s) in ${outputDir}` +\n\t\t\t(warnings ? ` (${warnings} with warnings)` : '') +\n\t\t\t(errors ? ` (${errors} with errors)` : '')\n\t)\n\n\tif (errors > 0) {\n\t\tprocess.exit(1)\n\t}\n}\n\nfunction printHelp(): void {\n\tconsole.log(`\nstonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes\n\nUSAGE:\n stonecrop-schema generate [options]\n\nSOURCE (exactly one required):\n --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint\n --introspection, -i <file> Read from a saved introspection JSON file\n --sdl, -s <file> Read from a GraphQL SDL (.graphql) file\n\nOUTPUT:\n --output, -o <dir> Directory to write doctype JSON files (required)\n\nOPTIONS:\n --include <types> Comma-separated list of type names to include\n --exclude <types> Comma-separated list of type names to exclude\n --overrides <file> JSON file with per-type field overrides\n --custom-scalars <file> JSON file mapping custom scalar names to field templates\n --include-unmapped Include _graphqlType metadata on unmapped fields\n --help, -h Show this help message\n\nEXAMPLES:\n # From a live PostGraphile server\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas\n\n # From a saved introspection result\n stonecrop-schema generate -i introspection.json -o ./schemas\n\n # From an SDL file with custom scalars\n stonecrop-schema generate -s schema.graphql -o ./schemas \\\\\n --custom-scalars custom-scalars.json\n\n # Only convert specific types\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\\\\n --include \"User,Post,Comment\"\n`)\n}\n\nmain().catch(err => {\n\tconsole.error('Error:', err.message)\n\tprocess.exit(1)\n})\n"],"names":["fetchIntrospection","endpoint","headers","response","getIntrospectionQuery","json","e","main","values","positionals","parseArgs","command","printHelp","outputDir","resolve","options","s","overridesPath","overridesContent","readFileSync","scalarsPath","scalarsContent","source","filePath","content","parsed","doctypes","convertGraphQLSchema","existsSync","mkdirSync","warnings","errors","doctype","fileName","join","writeFileSync","validation","validateDoctype","unmappedFields","f","err"],"mappings":";;;;;;AA8BA,eAAeA,EAAmBC,GAAkBC,GAA+D;AAClH,QAAMC,IAAW,MAAM,MAAMF,GAAU;AAAA,IACtC,QAAQ;AAAA,IACR,SAAS;AAAA,MACR,gBAAgB;AAAA,MAChB,GAAGC;AAAA,IAAA;AAAA,IAEJ,MAAM,KAAK,UAAU;AAAA,MACpB,OAAOE,EAAA;AAAA,IAAsB,CAC7B;AAAA,EAAA,CACD;AAED,MAAI,CAACD,EAAS;AACb,UAAM,IAAI,MAAM,kCAAkCA,EAAS,MAAM,IAAIA,EAAS,UAAU,EAAE;AAG3F,QAAME,IAAQ,MAAMF,EAAS,KAAA;AAK7B,MAAIE,EAAK,QAAQ;AAChB,UAAM,IAAI,MAAM,mBAAmBA,EAAK,OAAO,IAAI,CAAAC,MAAKA,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,EAAE;AAGhF,MAAI,CAACD,EAAK;AACT,UAAM,IAAI,MAAM,mCAAmC;AAGpD,SAAOA,EAAK;AACb;AAEA,eAAeE,IAAsB;AACpC,QAAM,EAAE,QAAAC,GAAQ,aAAAC,EAAA,IAAgBC,EAAU;AAAA,IACzC,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACR,UAAU,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MACnC,eAAe,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MACxC,KAAK,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MAC9B,QAAQ,EAAE,MAAM,UAAU,OAAO,IAAA;AAAA,MACjC,SAAS,EAAE,MAAM,SAAA;AAAA,MACjB,SAAS,EAAE,MAAM,SAAA;AAAA,MACjB,WAAW,EAAE,MAAM,SAAA;AAAA,MACnB,kBAAkB,EAAE,MAAM,SAAA;AAAA,MAC1B,oBAAoB,EAAE,MAAM,WAAW,SAAS,GAAA;AAAA,MAChD,MAAM,EAAE,MAAM,WAAW,OAAO,IAAA;AAAA,IAAI;AAAA,EACrC,CACA,GAEKC,IAAUF,EAAY,CAAC;AAE7B,GAAID,EAAO,QAAQ,CAACG,OACnBC,EAAA,GACA,QAAQ,KAAKD,IAAU,IAAI,CAAC,IAGzBA,MAAY,eACf,QAAQ,MAAM,oBAAoBA,CAAO,EAAE,GAC3C,QAAQ,MAAM,8BAA8B,GAC5C,QAAQ,KAAK,CAAC,IAIK,CAACH,EAAO,UAAUA,EAAO,eAAeA,EAAO,GAAG,EAAE,OAAO,OAAO,EAAE,WACpE,MACnB,QAAQ,MAAM,uEAAuE,GACrF,QAAQ,KAAK,CAAC,IAGVA,EAAO,WACX,QAAQ,MAAM,4BAA4B,GAC1C,QAAQ,KAAK,CAAC;AAGf,QAAMK,IAAYC,EAAQN,EAAO,MAAM,GAGjCO,IAAoC;AAAA,IACzC,qBAAqBP,EAAO,kBAAkB;AAAA,EAAA;AAW/C,MARIA,EAAO,YACVO,EAAQ,UAAUP,EAAO,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAAQ,MAAKA,EAAE,KAAA,CAAM,IAG1DR,EAAO,YACVO,EAAQ,UAAUP,EAAO,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAAQ,MAAKA,EAAE,KAAA,CAAM,IAG1DR,EAAO,WAAW;AACrB,UAAMS,IAAgBH,EAAQN,EAAO,SAAS,GACxCU,IAAmBC,EAAaF,GAAe,OAAO;AAC5D,IAAAF,EAAQ,gBAAgB,KAAK,MAAMG,CAAgB;AAAA,EACpD;AAEA,MAAIV,EAAO,gBAAgB,GAAG;AAC7B,UAAMY,IAAcN,EAAQN,EAAO,gBAAgB,CAAC,GAC9Ca,IAAiBF,EAAaC,GAAa,OAAO;AACxD,IAAAL,EAAQ,gBAAgB,KAAK,MAAMM,CAAc;AAAA,EAClD;AAGA,MAAIC;AAEJ,MAAId,EAAO;AACV,YAAQ,IAAI,+BAA+BA,EAAO,QAAQ,KAAK,GAC/Dc,IAAS,MAAMtB,EAAmBQ,EAAO,QAAQ;AAAA,WACvCA,EAAO,eAAe;AAChC,UAAMe,IAAWT,EAAQN,EAAO,aAAa,GACvCgB,IAAUL,EAAaI,GAAU,OAAO,GACxCE,IAAS,KAAK,MAAMD,CAAO;AAEjC,IAAAF,IAASG,EAAO,QAAQA;AAAA,EACzB,OAAO;AACN,UAAMF,IAAWT,EAAQN,EAAO,GAAI;AACpC,IAAAc,IAASH,EAAaI,GAAU,OAAO;AAAA,EACxC;AAGA,QAAMG,IAAWC,EAAqBL,GAAQP,CAAO;AAErD,EAAIW,EAAS,WAAW,MACvB,QAAQ,KAAK,0EAA0E,GACvF,QAAQ,KAAK,CAAC,IAIVE,EAAWf,CAAS,KACxBgB,EAAUhB,GAAW,EAAE,WAAW,GAAA,CAAM;AAGzC,MAAIiB,IAAW,GACXC,IAAS;AAEb,aAAWC,KAAWN,GAAU;AAC/B,UAAMO,IAAW,GAAGD,EAAQ,IAAI,SAC1BT,IAAWW,EAAKrB,GAAWoB,CAAQ,GACnC5B,IAAO,KAAK,UAAU2B,GAAS,MAAM,GAAI;AAE/C,IAAAG,EAAcZ,GAAUlB,IAAO;AAAA,GAAM,OAAO;AAG5C,UAAM+B,IAAaC,EAAgBL,CAAO;AAC1C,QAAKI,EAAW,SAMT;AAEN,YAAME,IAAiBN,EAAQ,OAAO,OAAO,CAACO,MAAWA,EAAE,SAAS;AACpE,MAAID,EAAe,SAAS,MAC3BR,KACA,QAAQ;AAAA,QACP,WAAWG,CAAQ,QAAQK,EAAe,MAAM,uBAAuBA,EACrE,IAAI,CAACC,MAAWA,EAAE,SAAS,EAC3B,KAAK,IAAI,CAAC;AAAA,MAAA;AAAA,IAGf,OAjByB;AACxB,MAAAR,KACA,QAAQ,MAAM,YAAYE,CAAQ,qBAAqB;AACvD,iBAAWO,KAAOJ,EAAW;AAC5B,gBAAQ,MAAM,OAAOI,EAAI,KAAK,KAAK,GAAG,CAAC,KAAKA,EAAI,OAAO,EAAE;AAAA,IAE3D;AAAA,EAYD;AAEA,UAAQ;AAAA,IACP;AAAA,YAAed,EAAS,MAAM,kBAAkBb,CAAS,MACvDiB,IAAW,KAAKA,CAAQ,oBAAoB,OAC5CC,IAAS,KAAKA,CAAM,kBAAkB;AAAA,EAAA,GAGrCA,IAAS,KACZ,QAAQ,KAAK,CAAC;AAEhB;AAEA,SAASnB,IAAkB;AAC1B,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAoCZ;AACD;AAEAL,IAAO,MAAM,CAAAiC,MAAO;AACnB,UAAQ,MAAM,UAAUA,EAAI,OAAO,GACnC,QAAQ,KAAK,CAAC;AACf,CAAC;"}
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Default heuristics for identifying entity types and fields in a GraphQL schema.
3
+ *
4
+ * These heuristics work across common GraphQL servers (PostGraphile, Hasura, Apollo, etc.)
5
+ * by detecting widely-adopted conventions like the Relay connection pattern.
6
+ *
7
+ * All heuristics can be overridden via the `isEntityType`, `isEntityField`, and
8
+ * `classifyField` options in `GraphQLConversionOptions`.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ import { isScalarType, isEnumType, isObjectType, isListType, isNonNullType, } from 'graphql';
13
+ import { buildScalarMap, INTERNAL_SCALARS } from './scalars';
14
+ import { toSlug, camelToLabel } from '../naming';
15
+ /**
16
+ * Suffixes that identify synthetic/framework types generated by GraphQL servers.
17
+ * Types ending with these suffixes are typically not entities.
18
+ */
19
+ const SYNTHETIC_SUFFIXES = [
20
+ 'Connection',
21
+ 'Edge',
22
+ 'Input',
23
+ 'Patch',
24
+ 'Payload',
25
+ 'Condition',
26
+ 'Filter',
27
+ 'OrderBy',
28
+ 'Aggregate',
29
+ 'AggregateResult',
30
+ 'AggregateFilter',
31
+ 'DeleteResponse',
32
+ 'InsertResponse',
33
+ 'UpdateResponse',
34
+ 'MutationResponse',
35
+ ];
36
+ /**
37
+ * Root operation type names that are never entities.
38
+ */
39
+ const ROOT_TYPE_NAMES = new Set(['Query', 'Mutation', 'Subscription']);
40
+ /**
41
+ * Default heuristic to determine if a GraphQL object type represents an entity.
42
+ * An entity type becomes a Stonecrop doctype.
43
+ *
44
+ * This heuristic excludes:
45
+ * - Introspection types (`__*`)
46
+ * - Root operation types (`Query`, `Mutation`, `Subscription`)
47
+ * - Types with synthetic suffixes (e.g., `*Connection`, `*Edge`, `*Input`)
48
+ * - Types starting with `Node` interface marker (exact match only)
49
+ *
50
+ * @param typeName - The GraphQL type name
51
+ * @param type - The GraphQL object type definition
52
+ * @returns `true` if this type should become a Stonecrop doctype
53
+ * @public
54
+ */
55
+ export function defaultIsEntityType(typeName, type) {
56
+ // Exclude introspection types
57
+ if (typeName.startsWith('__')) {
58
+ return false;
59
+ }
60
+ // Exclude root operation types
61
+ if (ROOT_TYPE_NAMES.has(typeName)) {
62
+ return false;
63
+ }
64
+ // Exclude the Node interface marker type
65
+ if (typeName === 'Node') {
66
+ return false;
67
+ }
68
+ // Exclude types matching synthetic suffixes
69
+ for (const suffix of SYNTHETIC_SUFFIXES) {
70
+ if (typeName.endsWith(suffix)) {
71
+ return false;
72
+ }
73
+ }
74
+ // Must have at least one field
75
+ const fields = type.getFields();
76
+ if (Object.keys(fields).length === 0) {
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+ /**
82
+ * Fields to skip by default on entity types.
83
+ * These are internal to GraphQL servers and don't represent semantic data.
84
+ */
85
+ const SKIP_FIELDS = new Set(['nodeId', '__typename', 'clientMutationId']);
86
+ /**
87
+ * Default heuristic to filter fields on entity types.
88
+ * Skips internal fields that don't represent meaningful data.
89
+ *
90
+ * @param fieldName - The GraphQL field name
91
+ * @param _field - The GraphQL field definition (unused in default implementation)
92
+ * @param _parentType - The parent entity type (unused in default implementation)
93
+ * @returns `true` if this field should be included
94
+ * @public
95
+ */
96
+ export function defaultIsEntityField(fieldName, _field, _parentType) {
97
+ return !SKIP_FIELDS.has(fieldName);
98
+ }
99
+ /**
100
+ * Unwrap NonNull and List wrappers from a GraphQL type, tracking nullability.
101
+ *
102
+ * @param type - The GraphQL output type
103
+ * @returns The unwrapped named type, whether it's required, and whether it's a list
104
+ * @internal
105
+ */
106
+ function unwrapType(type) {
107
+ let required = false;
108
+ let isList = false;
109
+ let current = type;
110
+ // Unwrap outer NonNull
111
+ if (isNonNullType(current)) {
112
+ required = true;
113
+ current = current.ofType;
114
+ }
115
+ // Unwrap List
116
+ if (isListType(current)) {
117
+ isList = true;
118
+ current = current.ofType;
119
+ // Unwrap inner NonNull (e.g., [Type!])
120
+ if (isNonNullType(current)) {
121
+ current = current.ofType;
122
+ }
123
+ }
124
+ // At this point, current should be a named type
125
+ return { namedType: current, required, isList };
126
+ }
127
+ /**
128
+ * Check if a GraphQL object type looks like a Relay Connection type.
129
+ * A connection type has an `edges` field returning a list of edge types,
130
+ * where each edge has a `node` field.
131
+ *
132
+ * @param type - The GraphQL object type to check
133
+ * @returns The node type name if this is a connection, or `undefined`
134
+ * @internal
135
+ */
136
+ function getConnectionNodeType(type) {
137
+ const fields = type.getFields();
138
+ // Must have an 'edges' field
139
+ const edgesField = fields['edges'];
140
+ if (!edgesField)
141
+ return undefined;
142
+ // edges must be a list
143
+ const { namedType: edgesType, isList: edgesIsList } = unwrapType(edgesField.type);
144
+ if (!edgesIsList || !isObjectType(edgesType))
145
+ return undefined;
146
+ // Each edge must have a 'node' field
147
+ const edgeFields = edgesType.getFields();
148
+ const nodeField = edgeFields['node'];
149
+ if (!nodeField)
150
+ return undefined;
151
+ const { namedType: nodeType } = unwrapType(nodeField.type);
152
+ if (!isObjectType(nodeType))
153
+ return undefined;
154
+ return nodeType.name;
155
+ }
156
+ /**
157
+ * Classify a single GraphQL field into a Stonecrop field definition.
158
+ *
159
+ * Classification rules (in order):
160
+ * 1. Scalar types → look up in merged scalar map
161
+ * 2. Enum types → `Select` with enum values as options
162
+ * 3. Object types that are entities → `Link` with slug as options
163
+ * 4. Object types that are Connections → `Doctype` with node type slug as options
164
+ * 5. List of entity type → `Doctype` with item type slug as options
165
+ * 6. Anything else → `Data` with `_unmapped: true`
166
+ *
167
+ * @param fieldName - The GraphQL field name
168
+ * @param field - The GraphQL field definition
169
+ * @param entityTypes - Set of type names classified as entities
170
+ * @param options - Conversion options (for custom scalars, unmapped meta, etc.)
171
+ * @returns The Stonecrop field definition
172
+ * @public
173
+ */
174
+ export function classifyFieldType(fieldName, field, entityTypes, options = {}) {
175
+ const { namedType, required, isList } = unwrapType(field.type);
176
+ const scalarMap = buildScalarMap(options.customScalars);
177
+ const base = {
178
+ fieldname: fieldName,
179
+ label: camelToLabel(fieldName),
180
+ component: 'ATextInput',
181
+ fieldtype: 'Data',
182
+ };
183
+ if (required) {
184
+ base.required = true;
185
+ }
186
+ // 1. Scalar types
187
+ if (isScalarType(namedType)) {
188
+ // Skip internal scalars (e.g., Cursor)
189
+ if (INTERNAL_SCALARS.has(namedType.name)) {
190
+ base._unmapped = true;
191
+ if (options.includeUnmappedMeta) {
192
+ base._graphqlType = namedType.name;
193
+ }
194
+ return base;
195
+ }
196
+ const template = scalarMap[namedType.name];
197
+ if (template) {
198
+ base.component = template.component;
199
+ base.fieldtype = template.fieldtype;
200
+ }
201
+ else {
202
+ // Unknown scalar — default to Data with unmapped marker
203
+ base._unmapped = true;
204
+ if (options.includeUnmappedMeta) {
205
+ base._graphqlType = namedType.name;
206
+ }
207
+ }
208
+ return base;
209
+ }
210
+ // 2. Enum types → Select
211
+ if (isEnumType(namedType)) {
212
+ base.component = 'ADropdown';
213
+ base.fieldtype = 'Select';
214
+ base.options = namedType.getValues().map(v => v.name);
215
+ return base;
216
+ }
217
+ // 3–5. Object types
218
+ if (isObjectType(namedType)) {
219
+ // 3. Direct reference to an entity type → Link
220
+ if (!isList && entityTypes.has(namedType.name)) {
221
+ base.component = 'ALink';
222
+ base.fieldtype = 'Link';
223
+ base.options = toSlug(namedType.name);
224
+ return base;
225
+ }
226
+ // 4. Connection type → Doctype (child table)
227
+ const connectionNodeTypeName = getConnectionNodeType(namedType);
228
+ if (connectionNodeTypeName && entityTypes.has(connectionNodeTypeName)) {
229
+ base.component = 'ATable';
230
+ base.fieldtype = 'Doctype';
231
+ base.options = toSlug(connectionNodeTypeName);
232
+ return base;
233
+ }
234
+ // 5. List of entity type → Doctype
235
+ if (isList && entityTypes.has(namedType.name)) {
236
+ base.component = 'ATable';
237
+ base.fieldtype = 'Doctype';
238
+ base.options = toSlug(namedType.name);
239
+ return base;
240
+ }
241
+ // Unknown object type — mark as unmapped
242
+ base._unmapped = true;
243
+ if (options.includeUnmappedMeta) {
244
+ base._graphqlType = namedType.name;
245
+ }
246
+ return base;
247
+ }
248
+ // Fallback — shouldn't normally be reached
249
+ base._unmapped = true;
250
+ if (options.includeUnmappedMeta) {
251
+ base._graphqlType = namedType.name;
252
+ }
253
+ return base;
254
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * GraphQL Introspection to Stonecrop Schema Converter
3
+ *
4
+ * Converts a standard GraphQL introspection result (or SDL string) into
5
+ * Stonecrop doctype schemas. Source-agnostic — works with any GraphQL server.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import { buildClientSchema, buildSchema, isObjectType } from 'graphql';
10
+ import { toSlug, pascalToSnake } from '../naming';
11
+ import { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
12
+ /**
13
+ * Convert a GraphQL schema to Stonecrop doctype schemas.
14
+ *
15
+ * Accepts either an `IntrospectionQuery` result object or an SDL string.
16
+ * Entity types are identified using heuristics (or a custom `isEntityType` function)
17
+ * and converted to `DoctypeMeta`-compatible JSON objects.
18
+ *
19
+ * @param source - GraphQL introspection result or SDL string
20
+ * @param options - Conversion options for controlling output format and behavior
21
+ * @returns Array of converted Stonecrop doctype definitions
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // From introspection result (fetched from any GraphQL server)
26
+ * const introspection = await fetchIntrospection('http://localhost:5000/graphql')
27
+ * const doctypes = convertGraphQLSchema(introspection)
28
+ *
29
+ * // From SDL string
30
+ * const sdl = fs.readFileSync('schema.graphql', 'utf-8')
31
+ * const doctypes = convertGraphQLSchema(sdl)
32
+ *
33
+ * // With PostGraphile custom scalars
34
+ * const doctypes = convertGraphQLSchema(introspection, {
35
+ * customScalars: {
36
+ * BigFloat: { component: 'ADecimalInput', fieldtype: 'Decimal' }
37
+ * }
38
+ * })
39
+ * ```
40
+ *
41
+ * @public
42
+ */
43
+ export function convertGraphQLSchema(source, options = {}) {
44
+ const schema = buildGraphQLSchema(source);
45
+ const typeMap = schema.getTypeMap();
46
+ // Determine the root operation type names to exclude
47
+ const rootTypeNames = new Set();
48
+ const queryType = schema.getQueryType();
49
+ const mutationType = schema.getMutationType();
50
+ const subscriptionType = schema.getSubscriptionType();
51
+ if (queryType)
52
+ rootTypeNames.add(queryType.name);
53
+ if (mutationType)
54
+ rootTypeNames.add(mutationType.name);
55
+ if (subscriptionType)
56
+ rootTypeNames.add(subscriptionType.name);
57
+ // Use custom or default entity type detector
58
+ const isEntityType = options.isEntityType ?? defaultIsEntityType;
59
+ // Phase 1: Identify all entity types
60
+ const entityTypes = new Set();
61
+ for (const [typeName, type] of Object.entries(typeMap)) {
62
+ if (!isObjectType(type))
63
+ continue;
64
+ // Always skip root operation types (even if custom isEntityType doesn't)
65
+ if (rootTypeNames.has(typeName))
66
+ continue;
67
+ if (isEntityType(typeName, type)) {
68
+ entityTypes.add(typeName);
69
+ }
70
+ }
71
+ // Phase 2: Apply include/exclude filters
72
+ let filteredEntityTypes = entityTypes;
73
+ if (options.include) {
74
+ const includeSet = new Set(options.include);
75
+ filteredEntityTypes = new Set([...entityTypes].filter(t => includeSet.has(t)));
76
+ }
77
+ if (options.exclude) {
78
+ const excludeSet = new Set(options.exclude);
79
+ filteredEntityTypes = new Set([...filteredEntityTypes].filter(t => !excludeSet.has(t)));
80
+ }
81
+ // Phase 3: Convert each entity type to a doctype
82
+ const isEntityField = options.isEntityField ?? defaultIsEntityField;
83
+ const deriveTableName = options.deriveTableName ?? ((typeName) => pascalToSnake(typeName));
84
+ const doctypes = [];
85
+ for (const typeName of filteredEntityTypes) {
86
+ const type = typeMap[typeName];
87
+ if (!isObjectType(type))
88
+ continue;
89
+ const fields = type.getFields();
90
+ const typeOverrides = options.typeOverrides?.[typeName];
91
+ const convertedFields = Object.entries(fields)
92
+ .filter(([fieldName, field]) => isEntityField(fieldName, field, type))
93
+ .map(([fieldName, field]) => {
94
+ // Check for full custom classification first
95
+ if (options.classifyField) {
96
+ const custom = options.classifyField(fieldName, field, type);
97
+ if (custom !== null && custom !== undefined) {
98
+ return {
99
+ fieldname: fieldName,
100
+ label: custom.label ?? fieldName,
101
+ component: custom.component ?? 'ATextInput',
102
+ fieldtype: custom.fieldtype ?? 'Data',
103
+ ...custom,
104
+ };
105
+ }
106
+ }
107
+ // Default classification
108
+ const classified = classifyFieldType(fieldName, field, entityTypes, options);
109
+ // Apply per-field overrides
110
+ if (typeOverrides?.[fieldName]) {
111
+ return { ...classified, ...typeOverrides[fieldName] };
112
+ }
113
+ return classified;
114
+ })
115
+ // Clean up internal metadata unless requested
116
+ .map(field => {
117
+ if (!options.includeUnmappedMeta) {
118
+ const { _graphqlType, _unmapped, ...clean } = field;
119
+ return clean;
120
+ }
121
+ return field;
122
+ });
123
+ const doctype = {
124
+ name: typeName,
125
+ slug: toSlug(typeName),
126
+ fields: convertedFields,
127
+ };
128
+ const tableName = deriveTableName(typeName);
129
+ if (tableName) {
130
+ doctype.tableName = tableName;
131
+ }
132
+ if (options.includeUnmappedMeta) {
133
+ doctype._graphqlTypeName = typeName;
134
+ }
135
+ doctypes.push(doctype);
136
+ }
137
+ return doctypes;
138
+ }
139
+ /**
140
+ * Build a GraphQLSchema from either an introspection result or SDL string.
141
+ *
142
+ * @param source - IntrospectionQuery object or SDL string
143
+ * @returns A complete GraphQLSchema
144
+ * @internal
145
+ */
146
+ function buildGraphQLSchema(source) {
147
+ if (typeof source === 'string') {
148
+ // SDL string
149
+ return buildSchema(source);
150
+ }
151
+ // IntrospectionQuery result
152
+ return buildClientSchema(source);
153
+ }
154
+ // ═══════════════════════════════════════════════════════════════
155
+ // Re-exports
156
+ // ═══════════════════════════════════════════════════════════════
157
+ // Main converter (this file)
158
+ export { convertGraphQLSchema as default };
159
+ // Scalar maps
160
+ export { GQL_SCALAR_MAP, WELL_KNOWN_SCALARS, INTERNAL_SCALARS, buildScalarMap } from './scalars';
161
+ // Heuristics
162
+ export { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
163
+ // Naming utilities
164
+ export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from '../naming';
@@ -0,0 +1,86 @@
1
+ /**
2
+ * GraphQL Scalar Type Mappings
3
+ *
4
+ * Maps standard GraphQL scalars and well-known custom scalars to Stonecrop field types.
5
+ * Source-agnostic — covers scalars commonly emitted by PostGraphile, Hasura, Apollo, etc.
6
+ *
7
+ * Users can extend these via the `customScalars` option in `GraphQLConversionOptions`.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ /**
12
+ * Mapping from standard GraphQL scalar types to Stonecrop field types.
13
+ * These are defined by the GraphQL specification and are always available.
14
+ *
15
+ * @public
16
+ */
17
+ export const GQL_SCALAR_MAP = {
18
+ String: { component: 'ATextInput', fieldtype: 'Data' },
19
+ Int: { component: 'ANumericInput', fieldtype: 'Int' },
20
+ Float: { component: 'ANumericInput', fieldtype: 'Float' },
21
+ Boolean: { component: 'ACheckbox', fieldtype: 'Check' },
22
+ ID: { component: 'ATextInput', fieldtype: 'Data' },
23
+ };
24
+ /**
25
+ * Mapping from well-known custom GraphQL scalars to Stonecrop field types.
26
+ * These cover scalars commonly used across GraphQL servers (PostGraphile, Hasura, etc.)
27
+ * without baking in knowledge of any specific server.
28
+ *
29
+ * Entries here have lower precedence than `customScalars` from options, but higher
30
+ * precedence than unknown/unmapped scalars.
31
+ *
32
+ * @public
33
+ */
34
+ export const WELL_KNOWN_SCALARS = {
35
+ // Arbitrary precision / large numbers
36
+ BigFloat: { component: 'ADecimalInput', fieldtype: 'Decimal' },
37
+ BigDecimal: { component: 'ADecimalInput', fieldtype: 'Decimal' },
38
+ Decimal: { component: 'ADecimalInput', fieldtype: 'Decimal' },
39
+ BigInt: { component: 'ANumericInput', fieldtype: 'Int' },
40
+ Long: { component: 'ANumericInput', fieldtype: 'Int' },
41
+ // Identifiers
42
+ UUID: { component: 'ATextInput', fieldtype: 'Data' },
43
+ // Date / Time
44
+ DateTime: { component: 'ADatetimePicker', fieldtype: 'Datetime' },
45
+ Datetime: { component: 'ADatetimePicker', fieldtype: 'Datetime' },
46
+ Date: { component: 'ADatePicker', fieldtype: 'Date' },
47
+ Time: { component: 'ATimeInput', fieldtype: 'Time' },
48
+ Interval: { component: 'ADurationInput', fieldtype: 'Duration' },
49
+ Duration: { component: 'ADurationInput', fieldtype: 'Duration' },
50
+ // Structured data
51
+ JSON: { component: 'ACodeEditor', fieldtype: 'JSON' },
52
+ JSONObject: { component: 'ACodeEditor', fieldtype: 'JSON' },
53
+ JsonNode: { component: 'ACodeEditor', fieldtype: 'JSON' },
54
+ };
55
+ /**
56
+ * Set of scalar type names that are internal to GraphQL servers and should be skipped
57
+ * during field conversion (they don't represent meaningful data fields).
58
+ *
59
+ * @public
60
+ */
61
+ export const INTERNAL_SCALARS = new Set(['Cursor']);
62
+ /**
63
+ * Build a merged scalar map from the built-in maps and user-provided custom scalars.
64
+ * Precedence (highest to lowest): customScalars → GQL_SCALAR_MAP → WELL_KNOWN_SCALARS
65
+ *
66
+ * @param customScalars - User-provided scalar overrides
67
+ * @returns Merged scalar map
68
+ * @internal
69
+ */
70
+ export function buildScalarMap(customScalars) {
71
+ const merged = { ...WELL_KNOWN_SCALARS };
72
+ // Standard scalars override well-known
73
+ for (const [key, value] of Object.entries(GQL_SCALAR_MAP)) {
74
+ merged[key] = value;
75
+ }
76
+ // Custom scalars override everything
77
+ if (customScalars) {
78
+ for (const [key, value] of Object.entries(customScalars)) {
79
+ merged[key] = {
80
+ component: value.component ?? 'ATextInput',
81
+ fieldtype: value.fieldtype ?? 'Data',
82
+ };
83
+ }
84
+ }
85
+ return merged;
86
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Types for the GraphQL introspection to Stonecrop schema converter.
3
+ * Source-agnostic — works with any GraphQL server (PostGraphile, Hasura, Apollo, etc.)
4
+ * @packageDocumentation
5
+ */
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+ import { FieldMeta } from './field';
3
+ /**
4
+ * Action definition within a workflow
5
+ * @public
6
+ */
7
+ export const ActionDefinition = z.object({
8
+ /** Display label for the action */
9
+ label: z.string().min(1),
10
+ /** Handler function name or path */
11
+ handler: z.string().min(1),
12
+ /** Fields that must have values before action can execute */
13
+ requiredFields: z.array(z.string()).optional(),
14
+ /** Workflow states where this action is available */
15
+ allowedStates: z.array(z.string()).optional(),
16
+ /** Whether to show a confirmation dialog */
17
+ confirm: z.boolean().optional(),
18
+ /** Additional arguments for the action */
19
+ args: z.record(z.string(), z.unknown()).optional(),
20
+ });
21
+ /**
22
+ * Workflow metadata - states and actions for a doctype
23
+ * @public
24
+ */
25
+ export const WorkflowMeta = z.object({
26
+ /** List of workflow states */
27
+ states: z.array(z.string()).optional(),
28
+ /** Actions available in this workflow */
29
+ actions: z.record(z.string(), ActionDefinition).optional(),
30
+ });
31
+ /**
32
+ * Doctype metadata - complete definition of a doctype
33
+ * @public
34
+ */
35
+ export const DoctypeMeta = z.object({
36
+ /** Display name of the doctype */
37
+ name: z.string().min(1),
38
+ /** URL-friendly slug (kebab-case) */
39
+ slug: z.string().min(1).optional(),
40
+ /** Database table name */
41
+ tableName: z.string().optional(),
42
+ /** Field definitions */
43
+ fields: z.array(FieldMeta),
44
+ /** Workflow configuration */
45
+ workflow: WorkflowMeta.optional(),
46
+ /** Parent doctype for inheritance */
47
+ inherits: z.string().optional(),
48
+ /** Doctype to use for list views */
49
+ listDoctype: z.string().optional(),
50
+ /** Parent doctype for child tables */
51
+ parentDoctype: z.string().optional(),
52
+ });