@bobbykim/manguito-cms-api 0.9.2 → 0.9.4
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.
- package/dist/{chunk-ITOSMGDT.mjs → chunk-DHOVO42E.mjs} +2 -2
- package/dist/{chunk-GUZZQZSN.mjs → chunk-EFTMAG7N.mjs} +207 -46
- package/dist/chunk-EFTMAG7N.mjs.map +1 -0
- package/dist/graphql/index.js +31 -16
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-5OTVPIAF.mjs +8 -0
- package/dist/index.js +349 -287
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +229 -320
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-GUZZQZSN.mjs.map +0 -1
- package/dist/handler-MFF32JB4.mjs +0 -8
- /package/dist/{chunk-ITOSMGDT.mjs.map → chunk-DHOVO42E.mjs.map} +0 -0
- /package/dist/{handler-MFF32JB4.mjs.map → handler-5OTVPIAF.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/graphql/index.ts","../../src/graphql/handler.ts","../../src/graphql/schema.ts","../../src/graphql/scalars.ts","../../src/graphql/type-mapping.ts","../../src/graphql/naming.ts","../../src/graphql/filters.ts","../../src/graphql/resolvers.ts","../../src/field-keys.ts","../../src/graphql/version-view.ts","../../src/graphql/dataloaders.ts","../../src/relations.ts","../../src/graphql/security.ts"],"sourcesContent":["export { createGraphQLHandler } from './handler.js'\nexport type { ResolvedGraphQLOptions } from './handler.js'\n","import { createYoga, maskError } from 'graphql-yoga'\nimport type { Handler } from 'hono'\nimport type { SchemaRegistry, ContentRepository, VersionProjection } from '@bobbykim/manguito-cms-core'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type { ProgrammaticResolver } from '../programmatic/resolve.js'\nimport type { FieldKeyMap } from '../field-keys.js'\nimport type { GraphQLContext } from './context.js'\nimport { buildGraphQLSchema } from './schema.js'\nimport { buildVersionView, type VersionView } from './version-view.js'\nimport { createRelationLoaders } from './dataloaders.js'\nimport { buildArmorPlugin, introspectionPlugin } from './security.js'\n\nexport type ResolvedGraphQLOptions = {\n enabled: boolean\n maxDepth: number\n maxComplexity: number\n graphiql: boolean\n introspection: boolean\n}\n\n// Yoga decides whether an error is safe to show a client with\n// `error instanceof GraphQLError`. That check is realm-sensitive: GraphQL Armor\n// is CJS and throws a GraphQLError built from graphql's CJS entry, while Yoga is\n// ESM and compares against the class from graphql's ESM entry. Same version, two\n// classes — so `instanceof` fails and a depth/complexity rejection, which is the\n// client's own query being too large, was masked as\n// `INTERNAL_SERVER_ERROR: Unexpected error.` and logged server-side as a fault.\n//\n// Identify by name instead (what envelop's own masking does), walking the\n// originalError chain the same way. A GraphQLError with no originalError was\n// constructed deliberately by a validation rule or by us and carries no internals;\n// anything else — a resolver throwing, a driver error — still goes to Yoga's\n// masking untouched, dev-mode originalError detail included.\nfunction isClientSafeGraphQLError(error: unknown): boolean {\n let current = error as { name?: string; originalError?: unknown } | null | undefined\n while (current != null && typeof current === 'object' && current.name === 'GraphQLError') {\n if (current.originalError == null) return true\n current = current.originalError as typeof current\n }\n return false\n}\n\nexport function createGraphQLHandler(\n registry: SchemaRegistry,\n repos: Record<string, ContentRepository<unknown>>,\n fieldKeyMaps: Record<string, FieldKeyMap>,\n resolver: ProgrammaticResolver,\n db: DrizzlePostgresInstance,\n options: ResolvedGraphQLOptions,\n // The view is built HERE, not in app.ts: the `.` entry must keep no static\n // import of anything under graphql/ (ADR api/0006), so app.ts passes the\n // plain projection data it already imports as types from core.\n versioning: {\n endpoint?: string\n projection?: VersionProjection\n currentProjection?: VersionProjection\n currentVersion?: string\n // Current's field-key maps. A programmatic resolver reads its record in\n // current's labels on every version; omitted, it reads `fieldKeyMaps`.\n currentFieldKeyMaps?: Record<string, FieldKeyMap>\n } = {}\n): Handler {\n let view: VersionView | undefined\n if (versioning.projection !== undefined) {\n if (versioning.currentVersion === undefined) {\n throw new Error(\n 'createGraphQLHandler: currentVersion is required when a projection is supplied — ' +\n 'deprecation reasons name it, and an empty one ships \"Removed in .\" into published SDL.'\n )\n }\n view = buildVersionView({\n registry,\n projection: versioning.projection,\n currentProjection: versioning.currentProjection,\n currentVersion: versioning.currentVersion,\n })\n }\n const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps)\n const { plugins } = buildArmorPlugin({\n maxDepth: options.maxDepth,\n maxComplexity: options.maxComplexity,\n })\n\n const yoga = createYoga<Record<string, never>, GraphQLContext>({\n schema,\n graphqlEndpoint: versioning.endpoint ?? '/graphql',\n graphiql: options.graphiql,\n landingPage: false,\n maskedErrors: {\n maskError: (error, message, isDev) =>\n isClientSafeGraphQLError(error) ? (error as Error) : maskError(error, message, isDev),\n },\n // buildArmorPlugin's return type is `unknown[]` (see security.ts) since Armor's\n // plugin shapes aren't part of its public typings; Yoga's `plugins` option only\n // accepts its own `Plugin` type, so the cast bridges the two.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n plugins: [...(plugins as any[]), introspectionPlugin(options.introspection)],\n context: (): GraphQLContext => ({\n db,\n registry,\n repos,\n resolver,\n loaders: createRelationLoaders(db, registry),\n programmaticMemo: new WeakMap(),\n }),\n })\n\n return (c) => yoga.fetch(c.req.raw, {})\n}\n","import {\n GraphQLSchema,\n GraphQLObjectType,\n GraphQLEnumType,\n GraphQLNonNull,\n GraphQLList,\n GraphQLID,\n GraphQLString,\n GraphQLInt,\n GraphQLBoolean,\n type GraphQLFieldConfig,\n type GraphQLOutputType,\n type GraphQLFieldConfigMap,\n} from 'graphql'\nimport type {\n SchemaRegistry,\n ParsedContentType,\n ParsedTaxonomyType,\n ParsedParagraphType,\n ParsedField,\n ParsedEnumType,\n} from '@bobbykim/manguito-cms-core'\nimport { relationCardinality } from '@bobbykim/manguito-cms-core'\nimport type { GraphQLContext } from './context.js'\nimport type { FieldKeyMap } from '../field-keys.js'\nimport type { VersionView, ViewField } from './version-view.js'\nimport { DateTimeScalar } from './scalars.js'\nimport { scalarOutputType } from './type-mapping.js'\nimport {\n graphqlTypeName,\n singleQueryName,\n collectionQueryName,\n toCamelCase,\n isValidGraphQLName,\n buildFieldNameMap,\n} from './naming.js'\nimport { SortOrderEnum, buildSortFieldEnum, buildFilterInputType } from './filters.js'\nimport {\n scalarFieldResolver,\n relationFieldResolver,\n programmaticFieldResolver,\n collectionResolver,\n singleBySlugResolver,\n singletonResolver,\n taxonomySingleResolver,\n} from './resolvers.js'\n\nconst PAGE_META = new GraphQLObjectType({\n name: 'PageMeta',\n fields: {\n total: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.total },\n page: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.page },\n perPage: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.per_page },\n totalPages: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.total_pages },\n hasNext: { type: new GraphQLNonNull(GraphQLBoolean), resolve: (m) => m.has_next },\n hasPrev: { type: new GraphQLNonNull(GraphQLBoolean), resolve: (m) => m.has_prev },\n },\n})\n\nconst MEDIA = new GraphQLObjectType({\n name: 'Media',\n fields: {\n id: { type: new GraphQLNonNull(GraphQLID), resolve: (m) => m.id },\n type: { type: new GraphQLNonNull(GraphQLString), resolve: (m) => m.type },\n url: { type: new GraphQLNonNull(GraphQLString), resolve: (m) => m.url },\n mimeType: { type: new GraphQLNonNull(GraphQLString), resolve: (m) => m.mime_type },\n alt: { type: GraphQLString, resolve: (m) => m.alt },\n fileSize: { type: GraphQLInt, resolve: (m) => m.file_size },\n width: { type: GraphQLInt, resolve: (m) => m.width },\n height: { type: GraphQLInt, resolve: (m) => m.height },\n duration: { type: GraphQLInt, resolve: (m) => m.duration },\n },\n})\n\nexport function buildGraphQLSchema(\n registry: SchemaRegistry,\n fieldKeyMaps: Record<string, FieldKeyMap> = {},\n view?: VersionView,\n // The maps a programmatic resolver's record is built with: current's, on\n // every version. Defaults to `fieldKeyMaps`, which is right when they ARE\n // current's (no view, or the current version's own endpoint).\n programmaticKeyMaps: Record<string, FieldKeyMap> = fieldKeyMaps\n): GraphQLSchema {\n const objectTypes = new Map<string, GraphQLObjectType>() // machineName → type\n const enumTypes = new Map<string, GraphQLEnumType>() // enum machineName → type (only when valid)\n\n // Enum types (only when every value is a valid GraphQL identifier).\n for (const [name, enumType] of Object.entries(registry.enum_types) as [string, ParsedEnumType][]) {\n if (enumType.values.every(isValidGraphQLName)) {\n enumTypes.set(\n name,\n new GraphQLEnumType({\n name: graphqlTypeName(name),\n values: Object.fromEntries(enumType.values.map((v) => [v, { value: v }])),\n })\n )\n } else {\n process.stderr.write(\n `⚠ enum '${name}' has values that aren't valid GraphQL identifiers; exposing as String\\n`\n )\n }\n }\n\n // Output type for one field (scalar, enum, relation, media, programmatic).\n function outputTypeForField(field: ParsedField, required: boolean): GraphQLOutputType {\n const scalar = scalarOutputType(field.field_type)\n if (scalar) return required ? new GraphQLNonNull(scalar) : scalar\n\n if (field.field_type === 'enum') {\n const ref = field.ui_component.component === 'select' ? field.ui_component.enum_ref : undefined\n const et = ref ? enumTypes.get(ref) : undefined\n const t = et ?? GraphQLString\n return required ? new GraphQLNonNull(t) : t\n }\n\n if (field.field_type === 'image' || field.field_type === 'video' || field.field_type === 'file') {\n return MEDIA\n }\n\n if (field.field_type === 'paragraph') {\n const ref = field.ui_component.component === 'paragraph-embed' ? field.ui_component.ref : undefined\n const target = ref ? objectTypes.get(ref) : undefined\n if (ref && !target) {\n process.stderr.write(\n `⚠ paragraph field '${field.name}' targets unknown paragraph type '${ref}'; exposing as Media\\n`\n )\n }\n const t = (target ?? MEDIA) as GraphQLObjectType\n return relationCardinality(field) === 'one' ? t : new GraphQLList(new GraphQLNonNull(t))\n }\n\n if (field.field_type === 'reference') {\n const ref = field.ui_component.component === 'typeahead-select' ? field.ui_component.ref : undefined\n const target = ref ? objectTypes.get(ref) : undefined\n if (ref && !target) {\n process.stderr.write(`⚠ reference field '${field.name}' targets unknown type '${ref}'; exposing as Media\\n`)\n }\n const isMany = relationCardinality(field) === 'many'\n const t = (target ?? MEDIA) as GraphQLObjectType\n return isMany ? new GraphQLList(new GraphQLNonNull(t)) : t\n }\n\n return GraphQLString\n }\n\n /**\n * A type's fields minus its tombstones.\n *\n * A tombstone (`removed: true`) is column-backed — the parser keeps its\n * column alive for older live versions — but the version being served must\n * never expose it. REST drops it via the FieldKeyMap's drop-set, which\n * `remap` applies; GraphQL resolves per field by column through\n * `resolveFieldValue` and never touches that map, so the exclusion has to\n * happen where the schema is BUILT. A field absent from the schema is a\n * validation error, so the retained value cannot reach a response at all.\n */\n function exposedFields(type: { fields: ParsedField[] }): ParsedField[] {\n return type.fields.filter((f) => f.removed !== true)\n }\n\n /**\n * The fields one schema exposes for a type, as ViewFields. With a view, the\n * served version's own list; without one, the registry's minus tombstones.\n */\n function viewFieldsFor(machineName: string, type: { fields: ParsedField[] }): ViewField[] {\n return (\n view?.[machineName]?.fields ??\n exposedFields(type).map((f) => ({ field: f, exposedAs: f.name, required: f.required }))\n )\n }\n\n // Build the object type for a content/taxonomy/paragraph type. Fields are a\n // thunk so relations can reference types created later (circular graphs).\n function buildObjectType(\n machineName: string,\n type: ParsedContentType | ParsedTaxonomyType | ParsedParagraphType\n ): GraphQLObjectType {\n const visible = viewFieldsFor(machineName, type)\n\n // Every live media field the type has NOW, by registry name. That is the\n // key ctx.loaders.load looks a field up by. The set is current's, not this\n // version's visible one: the programmatic record speaks current's labels,\n // and REST resolves every media field in the registry for it too.\n // Tombstones are excluded; current's map drops their column regardless.\n const mediaFieldNames = type.fields\n .filter(\n (f) =>\n f.removed !== true &&\n (f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n )\n .map((f) => f.name)\n\n return new GraphQLObjectType({\n name: graphqlTypeName(machineName),\n fields: () => {\n const fields: GraphQLFieldConfigMap<Record<string, unknown>, GraphQLContext> = {}\n // System fields.\n fields['id'] = { type: new GraphQLNonNull(GraphQLID), resolve: (p) => p['id'] }\n if (type.schema_type !== 'paragraph-type') {\n fields['published'] = { type: new GraphQLNonNull(GraphQLBoolean), resolve: (p) => p['published'] }\n }\n if (type.schema_type === 'content-type') {\n fields['slug'] = { type: new GraphQLNonNull(GraphQLString), resolve: (p) => p['slug'] }\n }\n fields['createdAt'] = { type: new GraphQLNonNull(DateTimeScalar), resolve: (p) => p['created_at'] }\n fields['updatedAt'] = { type: new GraphQLNonNull(DateTimeScalar), resolve: (p) => p['updated_at'] }\n\n for (const vf of visible) {\n const field = vf.field\n const gqlName = toCamelCase(vf.exposedAs)\n const outType = outputTypeForField(field, vf.required)\n let resolve: GraphQLFieldConfig<Record<string, unknown>, GraphQLContext>['resolve']\n if (field.field_type === 'programmatic') {\n resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName])\n } else if (\n field.field_type === 'reference' ||\n field.field_type === 'paragraph' ||\n field.field_type === 'image' ||\n field.field_type === 'video' ||\n field.field_type === 'file'\n ) {\n resolve = relationFieldResolver(machineName, field.name)\n } else {\n resolve = scalarFieldResolver(field, vf.fallback)\n }\n fields[gqlName] = {\n type: outType,\n resolve,\n // Conditional spread, not `deprecationReason: vf.deprecationReason`\n // — exactOptionalPropertyTypes forbids present-and-undefined.\n ...(vf.deprecationReason !== undefined && { deprecationReason: vf.deprecationReason }),\n }\n }\n return fields\n },\n })\n }\n\n // Pass 1: create every object type (empty-safe thunks resolve later).\n // Track GraphQL type name → machine name so two schemas that collapse to the\n // same GraphQL type name (e.g. content--author / taxonomy--author both → \"Author\")\n // fail loud here instead of surfacing as a cryptic GraphQLSchema build error.\n const typeNameOwners = new Map<string, string>() // graphqlTypeName → machineName\n function registerTypeName(machineName: string): void {\n const gqlName = graphqlTypeName(machineName)\n const owner = typeNameOwners.get(gqlName)\n if (owner !== undefined && owner !== machineName) {\n throw new Error(\n `GraphQL type name collision: '${gqlName}' is produced by both '${owner}' and '${machineName}'. ` +\n `Rename one schema's machine name segment so their GraphQL type names differ.`\n )\n }\n typeNameOwners.set(gqlName, machineName)\n }\n\n for (const [name, ct] of Object.entries(registry.content_types)) {\n registerTypeName(name)\n objectTypes.set(name, buildObjectType(name, ct))\n }\n for (const [name, tt] of Object.entries(registry.taxonomy_types)) {\n registerTypeName(name)\n objectTypes.set(name, buildObjectType(name, tt))\n }\n for (const [name, pt] of Object.entries(registry.paragraph_types)) {\n registerTypeName(name)\n objectTypes.set(name, buildObjectType(name, pt))\n }\n\n // Pass 2: build the root Query type.\n const queryFields: GraphQLFieldConfigMap<unknown, GraphQLContext> = {}\n\n for (const [name, ct] of Object.entries(registry.content_types) as [string, ParsedContentType][]) {\n const objType = objectTypes.get(name)!\n const visible = viewFieldsFor(name, ct)\n // The version's own labels: translateFilters maps a GraphQL name back to a\n // label, and columnFor then takes that label to the storage column. System\n // fields go in too, because buildFilterInputType advertises createdAt and\n // updatedAt on every filter. Without them `createdAt` would fall through as\n // its own column name. columnFor returns nothing for a system field, so\n // translateFilters falls back to the name itself, which IS its column.\n const nameMap = buildFieldNameMap([\n ...visible.map((v) => v.exposedAs),\n ...ct.system_fields.map((s) => s.name),\n ])\n\n if (ct.only_one) {\n queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) }\n continue\n }\n\n const listType = new GraphQLObjectType({\n name: `${graphqlTypeName(name)}List`,\n fields: {\n data: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(objType))), resolve: (r) => r.data },\n meta: { type: new GraphQLNonNull(PAGE_META), resolve: (r) => r.meta },\n },\n })\n const filterType = buildFilterInputType(ct, visible)\n\n queryFields[collectionQueryName(name)] = {\n type: new GraphQLNonNull(listType),\n args: {\n page: { type: GraphQLInt },\n perPage: { type: GraphQLInt },\n sortBy: { type: buildSortFieldEnum(graphqlTypeName(name), fieldKeyMaps[name]) },\n sortOrder: { type: SortOrderEnum },\n ...(filterType ? { filter: { type: filterType } } : {}),\n },\n resolve: collectionResolver(name, nameMap, fieldKeyMaps[name]),\n }\n queryFields[singleQueryName(name)] = {\n type: objType,\n args: { slug: { type: new GraphQLNonNull(GraphQLString) } },\n resolve: singleBySlugResolver(name),\n }\n }\n\n for (const [name, tt] of Object.entries(registry.taxonomy_types) as [string, ParsedTaxonomyType][]) {\n const objType = objectTypes.get(name)!\n const listType = new GraphQLObjectType({\n name: `${graphqlTypeName(name)}List`,\n fields: {\n data: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(objType))), resolve: (r) => r.data },\n meta: { type: new GraphQLNonNull(PAGE_META), resolve: (r) => r.meta },\n },\n })\n queryFields[collectionQueryName(name)] = {\n type: new GraphQLNonNull(listType),\n args: { page: { type: GraphQLInt }, perPage: { type: GraphQLInt } },\n resolve: collectionResolver(\n name,\n buildFieldNameMap(viewFieldsFor(name, tt).map((v) => v.exposedAs)),\n fieldKeyMaps[name]\n ),\n }\n queryFields[singleQueryName(name)] = {\n type: objType,\n args: { id: { type: new GraphQLNonNull(GraphQLID) } },\n resolve: taxonomySingleResolver(name),\n }\n }\n\n return new GraphQLSchema({\n query: new GraphQLObjectType({ name: 'Query', fields: queryFields }),\n })\n}\n","import { GraphQLScalarType, Kind, valueFromASTUntyped } from 'graphql'\n\nexport const DateTimeScalar = new GraphQLScalarType({\n name: 'DateTime',\n description: 'ISO-8601 date-time string',\n serialize(value) {\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') return value\n if (typeof value === 'number') return new Date(value).toISOString()\n throw new TypeError(`DateTime cannot serialize value: ${String(value)}`)\n },\n parseValue(value) {\n if (typeof value !== 'string') throw new TypeError('DateTime must be a string')\n return value\n },\n parseLiteral(ast) {\n if (ast.kind !== Kind.STRING) throw new TypeError('DateTime must be a string')\n return ast.value\n },\n})\n\nexport const JSONScalar = new GraphQLScalarType({\n name: 'JSON',\n description: 'Arbitrary JSON value',\n serialize: (value) => value,\n parseValue: (value) => value,\n parseLiteral: (ast, variables) => valueFromASTUntyped(ast, variables ?? undefined),\n})\n","import {\n GraphQLString,\n GraphQLInt,\n GraphQLFloat,\n GraphQLBoolean,\n type GraphQLScalarType,\n} from 'graphql'\nimport type { FieldType } from '@bobbykim/manguito-cms-core'\nimport { DateTimeScalar, JSONScalar } from './scalars.js'\n\n// Scalar output type for a field. Relation/enum fields return null — schema.ts\n// resolves those against its type caches.\nexport function scalarOutputType(fieldType: FieldType): GraphQLScalarType | null {\n switch (fieldType) {\n case 'text/plain':\n case 'text/rich':\n return GraphQLString\n case 'integer':\n return GraphQLInt\n case 'float':\n return GraphQLFloat\n case 'boolean':\n return GraphQLBoolean\n case 'date':\n return DateTimeScalar\n case 'programmatic':\n return JSONScalar\n default:\n return null\n }\n}\n","// \"content--blog_post\" → \"blog_post\"; \"category\" → \"category\"\nexport function schemaSegment(machineName: string): string {\n const idx = machineName.indexOf('--')\n return idx === -1 ? machineName : machineName.slice(idx + 2)\n}\n\nfunction words(input: string): string[] {\n return input.split(/[_\\s-]+/).filter(Boolean)\n}\n\nexport function toPascalCase(input: string): string {\n return words(input)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join('')\n}\n\nexport function toCamelCase(input: string): string {\n const pascal = toPascalCase(input)\n return pascal.charAt(0).toLowerCase() + pascal.slice(1)\n}\n\n// Deterministic English pluralization for the common cases; irregular plurals\n// (person→people) are a known limitation — an optional per-type override can be\n// added later if one is wrong.\nexport function pluralize(word: string): string {\n if (/[^aeiou]y$/.test(word)) return word.slice(0, -1) + 'ies'\n if (/(s|x|z|ch|sh)$/.test(word)) return word + 'es'\n return word + 's'\n}\n\nconst GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/\nexport function isValidGraphQLName(name: string): boolean {\n return GRAPHQL_NAME.test(name)\n}\n\nexport function graphqlTypeName(machineName: string): string {\n return toPascalCase(schemaSegment(machineName))\n}\n\nexport function singleQueryName(machineName: string): string {\n return toCamelCase(schemaSegment(machineName))\n}\n\nexport function collectionQueryName(machineName: string): string {\n return pluralize(singleQueryName(machineName))\n}\n\nexport function buildFieldNameMap(schemaNames: string[]): {\n toGraphql(schemaName: string): string\n toSchema(graphqlName: string): string\n} {\n const toG = new Map<string, string>()\n const toS = new Map<string, string>()\n for (const name of schemaNames) {\n const g = toCamelCase(name)\n toG.set(name, g)\n toS.set(g, name)\n }\n return {\n toGraphql: (schemaName) => toG.get(schemaName) ?? toCamelCase(schemaName),\n toSchema: (graphqlName) => toS.get(graphqlName) ?? graphqlName,\n }\n}\n","import {\n GraphQLEnumType,\n GraphQLInputObjectType,\n GraphQLList,\n GraphQLNonNull,\n GraphQLString,\n GraphQLBoolean,\n GraphQLInt,\n GraphQLFloat,\n type GraphQLInputType,\n} from 'graphql'\nimport type {\n ParsedContentType,\n ParsedTaxonomyType,\n ParsedField,\n FilterValue,\n FilterOperator,\n FieldType,\n} from '@bobbykim/manguito-cms-core'\nimport { graphqlTypeName, buildFieldNameMap } from './naming.js'\nimport { scalarOutputType } from './type-mapping.js'\nimport { DateTimeScalar } from './scalars.js'\nimport type { FieldKeyMap } from '../field-keys.js'\nimport type { ViewField } from './version-view.js'\n\nexport const SortOrderEnum = new GraphQLEnumType({\n name: 'SortOrder',\n values: { ASC: { value: 'asc' }, DESC: { value: 'desc' } },\n})\n\n// Only these fields are sortable (mirrors the REST SORTABLE_FIELDS).\n//\n// `value` is the enum's internal value, and it is NOT uniformly a storage\n// column: `created_at` / `updated_at` are the snake_case forms of real system\n// columns, but `title` is a schema field's LABEL — the same value REST accepts\n// in `?sort_by=`. `collectionResolver` maps it to a column through the type's\n// FieldKeyMap before the repository builds its ORDER BY.\nconst SORTABLE: Array<{ gql: string; value: string }> = [\n { gql: 'title', value: 'title' },\n { gql: 'createdAt', value: 'created_at' },\n { gql: 'updatedAt', value: 'updated_at' },\n]\n\n/**\n * `fieldKeys` is THIS version's map. `title` in SORTABLE is a schema field's\n * LABEL, not a column, so a version that has no field with that label cannot\n * resolve it — and offering it anyway hands the repository a label that maps\n * to nothing, which surfaces as a 500 from SQL rather than a rejected query.\n * Mirrors `sortableColumnsFor` in versions.ts.\n *\n * `createdAt`/`updatedAt` are real system columns on every content type, so\n * they are always offered and the enum can never end up empty — which\n * GraphQL rejects.\n *\n * With no map, every value is kept: callers that have not threaded a\n * FieldKeyMap through behave exactly as before.\n */\nexport function buildSortFieldEnum(typeName: string, fieldKeys?: FieldKeyMap): GraphQLEnumType {\n const resolvable = SORTABLE.filter(\n (s) =>\n s.gql === 'createdAt' ||\n s.gql === 'updatedAt' ||\n fieldKeys === undefined ||\n fieldKeys.columnFor(s.value) !== undefined\n )\n return new GraphQLEnumType({\n name: `${typeName}SortField`,\n values: Object.fromEntries(resolvable.map((s) => [s.gql, { value: s.value }])),\n })\n}\n\n// A comparable filter input (eq/in/gt/gte/lt/lte) for a given scalar input type.\nfunction comparableFilterInput(name: string, scalar: GraphQLInputType): GraphQLInputObjectType {\n return new GraphQLInputObjectType({\n name,\n fields: {\n eq: { type: scalar },\n in: { type: new GraphQLList(new GraphQLNonNull(scalar)) },\n gt: { type: scalar },\n gte: { type: scalar },\n lt: { type: scalar },\n lte: { type: scalar },\n },\n })\n}\n\nfunction equalityFilterInput(name: string, scalar: GraphQLInputType): GraphQLInputObjectType {\n return new GraphQLInputObjectType({\n name,\n fields: {\n eq: { type: scalar },\n in: { type: new GraphQLList(new GraphQLNonNull(scalar)) },\n },\n })\n}\n\n// Shared per-scalar filter inputs (built once).\nconst StringFilter = equalityFilterInput('StringFilter', GraphQLString)\nconst BooleanFilter = new GraphQLInputObjectType({\n name: 'BooleanFilter',\n fields: { eq: { type: GraphQLBoolean } },\n})\nconst DateTimeFilter = comparableFilterInput('DateTimeFilter', DateTimeScalar)\nconst IDFilter = equalityFilterInput('IDFilter', GraphQLString)\nconst IntFilter = comparableFilterInput('IntFilter', GraphQLInt)\nconst FloatFilter = comparableFilterInput('FloatFilter', GraphQLFloat)\n\n// Choose a filter input for a field based on its scalar output type.\nfunction filterInputForField(fieldType: FieldType): GraphQLInputObjectType | null {\n const scalar = scalarOutputType(fieldType)\n if (fieldType === 'boolean') return BooleanFilter\n if (fieldType === 'date') return DateTimeFilter\n if (fieldType === 'integer') return IntFilter\n if (fieldType === 'float') return FloatFilter\n if (fieldType === 'reference') return IDFilter\n if (scalar === null) return null // only 'enum' reaches here — not filterable via a scalar input\n return StringFilter\n}\n\n// Build the <Type>Filter input. Programmatic fields are excluded (no column).\nexport function buildFilterInputType(\n type: ParsedContentType | ParsedTaxonomyType,\n viewFields?: ViewField[]\n): GraphQLInputObjectType | null {\n const fields: Record<string, { type: GraphQLInputObjectType }> = {}\n\n // System fields that are filterable columns.\n fields['id'] = { type: IDFilter }\n fields['published'] = { type: BooleanFilter }\n fields['createdAt'] = { type: DateTimeFilter }\n fields['updatedAt'] = { type: DateTimeFilter }\n if (type.schema_type === 'content-type') fields['slug'] = { type: StringFilter }\n\n // A view states the label THIS version exposes each field under; without\n // one, fall back to the field's own name minus tombstones (Task 2's rule).\n const entries: Array<{ field: ParsedField; exposedAs: string }> =\n viewFields !== undefined\n ? viewFields.map((v) => ({ field: v.field, exposedAs: v.exposedAs }))\n : type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name }))\n\n for (const { field: f, exposedAs } of entries) {\n if (f.field_type === 'programmatic' || f.field_type === 'paragraph') continue\n if (f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file') continue\n const input = filterInputForField(f.field_type)\n if (input) fields[buildFieldNameMap([exposedAs]).toGraphql(exposedAs)] = { type: input }\n }\n\n if (Object.keys(fields).length === 0) return null\n return new GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields })\n}\n\n// Convert a GraphQL filter argument into the repository's filters map, keyed by\n// snake_case column. eq → scalar, in → array (OR/IN), gt/gte/lt/lte → operator.\n//\n// `nameMap.toSchema` only ever recovers the schema field's LABEL (`field.name`)\n// from the GraphQL name — that mapping is unaware of storage columns. `columnFor`\n// (typically `FieldKeyMap.columnFor`) takes that label the rest of the way to the\n// storage column the repository actually filters on. It's optional so callers\n// that haven't threaded a FieldKeyMap through yet keep working unchanged: with no\n// mapper, or when it returns nothing for a given label, the label itself is used —\n// correct as long as it equals its column.\nexport function translateFilters(\n input: Record<string, unknown> | undefined,\n nameMap: { toSchema(g: string): string },\n columnFor?: (label: string) => string | undefined\n): Record<string, FilterValue> {\n const out: Record<string, FilterValue> = {}\n if (!input) return out\n\n for (const [gqlField, raw] of Object.entries(input)) {\n if (raw === null || typeof raw !== 'object') continue\n const label = nameMap.toSchema(gqlField)\n const column = columnFor?.(label) ?? label\n const spec = raw as Record<string, unknown>\n\n // eq/in take priority over range operators (gt/gte/lt/lte) when a client sends\n // both in one field filter — the range operators are silently dropped.\n if (spec['eq'] !== undefined) {\n out[column] = spec['eq'] as FilterValue\n } else if (Array.isArray(spec['in'])) {\n out[column] = spec['in'] as FilterValue\n } else {\n const op: FilterOperator = {}\n for (const k of ['gt', 'gte', 'lt', 'lte'] as const) {\n if (spec[k] !== undefined) op[k] = spec[k] as number | string\n }\n if (Object.keys(op).length > 0) out[column] = op\n }\n }\n return out\n}\n","import { GraphQLError } from 'graphql'\nimport type { ParsedField } from '@bobbykim/manguito-cms-core'\nimport type { GraphQLContext } from './context.js'\nimport { translateFilters } from './filters.js'\nimport { isColumnBacked, type FieldKeyMap } from '../field-keys.js'\n\ntype Row = Record<string, unknown>\n\nfunction codeError(code: string, message: string): GraphQLError {\n return new GraphQLError(message, { extensions: { code } })\n}\n\nfunction isPublished(item: Row | null): boolean {\n return !!item && item['published'] === true\n}\n\n/**\n * A GraphQL field's value on a content row. The GraphQL field NAME comes from\n * the field's label (see naming.ts), but the value lives under its storage\n * column, which differs once a field has been renamed.\n *\n * `fallback` is the value a version declares for a column that stopped being\n * written: rows created since the removal hold null there. Substituted on\n * null/undefined ONLY — never on '' or 0 — matching projectRow's rule, so the\n * REST and GraphQL surfaces cannot disagree about the same version.\n */\nexport function resolveFieldValue(\n field: ParsedField,\n row: Record<string, unknown>,\n fallback?: unknown\n): unknown {\n const key = isColumnBacked(field) ? field.db_column.column_name : field.name\n const value = row[key]\n if ((value === null || value === undefined) && fallback !== undefined) return fallback\n return value ?? null\n}\n\nexport function scalarFieldResolver(field: ParsedField, fallback?: unknown) {\n return (parent: Row): unknown => resolveFieldValue(field, parent, fallback)\n}\n\nexport function relationFieldResolver(typeName: string, schemaFieldName: string) {\n return (parent: Row, _args: unknown, ctx: GraphQLContext): Promise<unknown> =>\n ctx.loaders.load(typeName, schemaFieldName, parent)\n}\n\nexport function programmaticFieldResolver(\n typeName: string,\n schemaFieldName: string,\n mediaFieldNames: readonly string[] = [],\n fieldKeys?: FieldKeyMap\n) {\n return async (parent: Row, _args: unknown, ctx: GraphQLContext): Promise<unknown> => {\n let p = ctx.programmaticMemo.get(parent)\n if (!p) {\n p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys)\n ctx.programmaticMemo.set(parent, p)\n }\n return (await p)[schemaFieldName]\n }\n}\n\n// A programmatic resolver reads its record through `ctx.get()`, so that record\n// must look the same on both public surfaces. REST resolves media fields to full\n// objects before running resolvers; GraphQL reads through repositories that\n// resolve nothing, so without this `ctx.get('hero').url` would work over REST and\n// silently fall back to null over GraphQL.\n//\n// Media is resolved into a COPY, never the row itself: the programmatic record is\n// built from a copy so that projecting it to current's labels (toLabels below)\n// never touches the parent row, which the field resolvers still read by column.\n// The dataloaders resolve relations in place and idempotently now, so handing the\n// parent itself back to the loader would no longer break anything either — the\n// copy is kept to keep this path from mutating the parent, and it is harmless:\n// loaders still batch across the copy, so this costs no extra query.\n//\n// `ctx.get(fieldName)` takes the schema field name (the LABEL), so the record is\n// projected to CURRENT's labels (the caller passes current's map on every\n// version) before the resolver runs — the same order REST uses (see \"Response\n// projection order\" in routes/content.ts). GraphQL has no route-level\n// projection to double up with: every other field resolves per field by column\n// (resolveFieldValue), so this copy is the only mapped row, and it is mapped\n// strictly after the media loaders have run.\nasync function resolveProgrammaticRow(\n typeName: string,\n parent: Row,\n ctx: GraphQLContext,\n mediaFieldNames: readonly string[],\n fieldKeys?: FieldKeyMap\n): Promise<Record<string, unknown>> {\n const toLabels = (row: Row): Row => (fieldKeys ? fieldKeys.toLabels(row) : row)\n\n if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent))\n\n const enriched: Row = { ...parent }\n await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)))\n // The loaders resolve each media object IN PLACE under its storage column,\n // so toLabels carries it to its label like any scalar.\n return ctx.resolver.resolveItem(typeName, toLabels(enriched))\n}\n\ntype CollectionArgs = {\n page?: number\n perPage?: number\n /**\n * The sort enum's internal value (filters.ts `SORTABLE`), which mixes two key\n * spaces: `created_at` / `updated_at` are system COLUMNS, but `title` is a\n * schema field's LABEL. It must be mapped before it reaches the repository.\n */\n sortBy?: string\n sortOrder?: 'asc' | 'desc'\n filter?: Record<string, unknown>\n}\n\nexport function collectionResolver(\n typeName: string,\n nameMap: { toSchema(g: string): string },\n fieldKeys?: FieldKeyMap\n) {\n return async (_root: unknown, args: CollectionArgs, ctx: GraphQLContext) => {\n const page = args.page ?? 1\n const perPage = args.perPage ?? 10\n if (!Number.isInteger(page) || page < 1) {\n throw codeError('INVALID_PAGINATION', 'page must be ≥ 1')\n }\n if (!Number.isInteger(perPage) || perPage < 1 || perPage > 100) {\n throw codeError('INVALID_PAGINATION', 'perPage must be between 1 and 100')\n }\n const repo = ctx.repos[typeName]!\n // `ORDER BY` takes a storage column, never a label. `columnFor` resolves a\n // schema field's label to its column; a system column (`created_at`,\n // `updated_at`) is not a label at all, so it has no entry and falls\n // through unchanged. Identity today, since every label equals its column.\n const sortBy = args.sortBy ?? 'created_at'\n const sortColumn = fieldKeys?.columnFor(sortBy) ?? sortBy\n const result = await repo.findMany({\n published_only: true,\n page,\n per_page: perPage,\n sort_by: sortColumn as 'title' | 'created_at' | 'updated_at',\n sort_order: args.sortOrder ?? 'asc',\n filters: translateFilters(args.filter, nameMap, fieldKeys?.columnFor),\n })\n return { data: result.data as Row[], meta: result.meta }\n }\n}\n\nexport function singleBySlugResolver(typeName: string) {\n return async (_root: unknown, args: { slug: string }, ctx: GraphQLContext): Promise<Row | null> => {\n const repo = ctx.repos[typeName]!\n const item = (await repo.findBySlug(args.slug)) as Row | null\n return isPublished(item) ? item : null\n }\n}\n\nexport function singletonResolver(typeName: string) {\n return async (_root: unknown, _args: unknown, ctx: GraphQLContext): Promise<Row | null> => {\n const repo = ctx.repos[typeName]!\n const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 })\n return (result.data[0] as Row | undefined) ?? null\n }\n}\n\nexport function taxonomySingleResolver(typeName: string) {\n return async (_root: unknown, args: { id: string }, ctx: GraphQLContext): Promise<Row | null> => {\n const repo = ctx.repos[typeName]!\n const item = (await repo.findOne(args.id)) as Row | null\n return isPublished(item) ? item : null\n }\n}\n","import type { DbColumn, ParsedField } from '@bobbykim/manguito-cms-core'\n\n// ─── Label ↔ storage key mapping ──────────────────────────────────────────────\n//\n// A field has two names: its public LABEL (`field.name`, what API consumers and\n// the admin panel see) and its STORAGE key (`db_column.column_name`, the actual\n// Postgres column). They are identical today, but schema versioning makes a\n// rename change only the label — the column keeps its original name for the life\n// of the data. Every place that reads or writes a DB row must therefore use the\n// storage key, and every place that reads a request body or writes a response\n// must use the label.\n//\n// Only column-backed fields participate. Paragraph fields have no column\n// (their association lives on the paragraph table via parent_field) and\n// many-to-many references have no column either (the junction table owns the\n// association), so both keep the field name as their identity and are excluded.\n\n// A type predicate, not a plain boolean: narrowing `db_column` to non-null lets\n// the compiler enforce the invariant at every call site instead of each one\n// re-asserting it with `db_column!`.\nexport function isColumnBacked(field: ParsedField): field is ParsedField & { db_column: DbColumn } {\n const col = field.db_column\n if (col === null) return false\n if (col.column_name === '') return false\n if (col.junction) return false\n return true\n}\n\nexport type FieldKeyMap = {\n /** Request body (label-keyed) → storage-keyed. Unknown keys pass through. */\n toStorage(input: Record<string, unknown>): Record<string, unknown>\n /** DB row (storage-keyed) → label-keyed. Unknown keys pass through. */\n toLabels(row: Record<string, unknown>): Record<string, unknown>\n columnFor(label: string): string | undefined\n labelFor(column: string): string | undefined\n /** Labels of column-backed fields — the valid filter/sort surface. */\n labels: string[]\n /** False when every label equals its column, letting callers skip the copy. */\n diverges: boolean\n}\n\n/**\n * One field's place in the label space: the label it is known by, plus the\n * storage column it owns. `ownColumn` is ABSENT — never present-and-undefined,\n * per exactOptionalPropertyTypes — for a field with no column of its own.\n */\ntype LabelEntry = { label: string; ownColumn?: string }\n\n/**\n * The shared core: given a label↔column mapping and the FULL label space to\n * check against, produce a FieldKeyMap. Both `createFieldKeyMap` and\n * `createFieldKeyMapFromProjection` are thin adapters over this — the\n * collision check, the `diverges` computation and `remap` live here exactly\n * once.\n *\n * `pairs` may include entries that must NOT survive into the final map —\n * `createFieldKeyMap` passes tombstoned columns in alongside live ones,\n * exactly as the original single-function implementation did, so that the\n * collision check below (which runs against the unfiltered map) still\n * catches a paragraph/junction/programmatic field's label colliding with a\n * TOMBSTONE's column. Any pair whose label appears in `droppedLabels`, or\n * whose column appears in `droppedColumns`, is stripped from\n * `labelToColumn`/`columnToLabel` respectively, immediately after the check,\n * before `diverges` and `labels` are computed — so the caller does not need\n * to pre-filter `pairs` itself, only tell this function what to drop in each\n * key space.\n *\n * `allLabels` is every field's label, including fields with no column of\n * their own. That is not tidiness: a paragraph, many-to-many or programmatic\n * field's label is written into the same key space as storage columns before\n * `toLabels` runs, so one named after another field's column would overwrite\n * that column's value and then be renamed onto the other field's label.\n * Both constructors must pass the complete set.\n *\n * Each entry carries `ownColumn` — the column that field itself is backed by,\n * absent for a paragraph, many-to-many or programmatic field. The collision\n * check below needs it to tell which fields have no column of their own:\n * only those can collide, since a column-backed field's name is never a row\n * key.\n *\n * `droppedColumns`/`droppedLabels` also drive `remap`: each must actively\n * remove a key from a mapped object rather than merely leave it unmapped,\n * because `remap` passes an unmapped key through unchanged — a\n * retained-but-unexposed column would otherwise reach the output under its\n * raw name. They are kept as two SEPARATE sets, not one shared set, because\n * `toLabels` reads column-keyed input and `toStorage` reads label-keyed\n * input: a label and a column can be the same text, and one shared set would\n * let dropping one strip the other's unrelated pair.\n */\nfunction buildFieldKeyMap(\n pairs: Array<{ label: string; column: string }>,\n allLabels: LabelEntry[],\n droppedColumns: Set<string>,\n droppedLabels: Set<string>\n): FieldKeyMap {\n const labelToColumn = new Map<string, string>()\n const columnToLabel = new Map<string, string>()\n for (const { label, column } of pairs) {\n labelToColumn.set(label, column)\n columnToLabel.set(column, label)\n }\n\n // A label that is also some OTHER field's column name would make toLabels\n // ambiguous: two source keys would map onto one destination key.\n //\n // Checked against EVERY label in `allLabels`, not just the ones with a\n // pair of their own. Paragraph, many-to-many and programmatic fields have\n // no column of their own, but their labels are written into the same key\n // space as storage columns before toLabels runs (routes/admin/content.ts,\n // relations.ts, programmatic/resolve.ts). A paragraph field named after\n // another field's column would overwrite that column's value on the row\n // and then be renamed onto the other field's label — serving the wrong\n // value under the wrong key.\n //\n // Deliberately run BEFORE dropped pairs are stripped from `columnToLabel`\n // below: a paragraph/junction/programmatic field's label may collide with\n // a TOMBSTONE's column (the column still physically exists on the row),\n // and that must keep throwing. Stripping first would silently pass this\n // configuration. A column-backed field's label can no longer collide at\n // all (the skip below excludes it), live or tombstoned — only a field\n // with no column of its own is ever checked.\n //\n // Not detected: two fields declaring the SAME column name. A field can now\n // declare its own `column`, so this is no longer structurally impossible —\n // but core rejects it with `DUPLICATE_COLUMN` at parse time, so it is still\n // unreachable here.\n for (const { label, ownColumn } of allLabels) {\n // Only a field with no column of its own can collide. Its NAME is written\n // into the row as a key — paragraph, junction and programmatic values land\n // under it — so a name equal to some column would be relabelled as that\n // column's field. A column-backed field's name is never a row key: its\n // value lives under its column, and toLabels maps the column, not the name.\n if (ownColumn !== undefined) continue\n const columnOwner = columnToLabel.get(label)\n if (columnOwner !== undefined && columnOwner !== label) {\n throw new Error(\n `Fatal: field key map failed to build — field label \"${label}\" collides with the ` +\n `storage column of field \"${columnOwner}\". A field label may not reuse another ` +\n `field's column name. Rename the field, then run \\`manguito validate\\` to check your schema.`\n )\n }\n }\n\n // Only now strip dropped pairs — the collision check above has already run\n // against the full map. Each map is stripped by the set for ITS key space:\n // labelToColumn is keyed by label, columnToLabel by column. One shared set\n // let a label and a column of the same text strip each other's pair.\n for (const label of labelToColumn.keys()) {\n if (droppedLabels.has(label)) labelToColumn.delete(label)\n }\n for (const column of columnToLabel.keys()) {\n if (droppedColumns.has(column)) columnToLabel.delete(column)\n }\n\n const diverges = [...labelToColumn].some(([label, column]) => label !== column)\n\n function remap(\n input: Record<string, unknown>,\n lookup: Map<string, string>,\n dropped: Set<string>,\n droppedIfUnmapped?: Set<string>\n ): Record<string, unknown> {\n // Always returns a NEW object, even when nothing diverges. Returning `input`\n // unchanged would make aliasing depend on the schema, so a bug where a\n // caller mutates a row after mapping would reproduce only on renamed\n // fields. A shallow copy per row is not worth that class of bug.\n //\n // A key that IS a live label/column is mapped first, unconditionally —\n // `lookup` itself already excludes anything dropped (the strip loops\n // above), so a mapped hit can only be a live field. Only once a key is\n // found UNMAPPED do the drop sets apply, and `toStorage` checks both of\n // them there: `dropped` (droppedLabels — a tombstone's own name, refusing\n // a write addressed to it) and `droppedIfUnmapped` (droppedColumns — a\n // raw, unexposed column supplied directly, such as a tombstone's retained\n // column). Mapping first is what lets a live label write even when its\n // text happens to equal a tombstone's column (`description`/`d2` live,\n // `x`/`description` tombstone: `description` still maps to `d2`) while an\n // unmapped raw `description` would still be refused if it were itself a\n // dropped column. `toLabels` passes no `droppedIfUnmapped`, so it is\n // unaffected: a dropped column was already stripped from `lookup` and so\n // is always \"unmapped\", and is refused by `dropped` exactly as before.\n //\n // Unknown (and undropped) keys pass through unchanged.\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(input)) {\n const mapped = lookup.get(key)\n if (mapped !== undefined) {\n out[mapped] = input[key]\n continue\n }\n if (dropped.has(key) || droppedIfUnmapped?.has(key)) continue\n out[key] = input[key]\n }\n return out\n }\n\n return {\n // toStorage reads LABEL-keyed input: a live label maps and writes\n // regardless of either drop set; an unmapped key is refused if it is\n // EITHER the tombstone's own name (droppedLabels) or a raw, unexposed\n // column (droppedColumns) — see remap's comment.\n // toLabels reads COLUMN-keyed input, so it consults only droppedColumns.\n toStorage: (input) => remap(input, labelToColumn, droppedLabels, droppedColumns),\n toLabels: (row) => remap(row, columnToLabel, droppedColumns),\n columnFor: (label) => labelToColumn.get(label),\n labelFor: (column) => columnToLabel.get(column),\n labels: [...labelToColumn.keys()],\n diverges,\n }\n}\n\n/**\n * Built ONCE per content type at startup, not per request. Throws on a key\n * collision, matching how createCmsApp refuses to boot on a broken roles\n * registry: an ambiguous mapping would silently corrupt responses.\n *\n * Tombstones (`field.removed === true`) are column-backed — the parser keeps\n * their column alive for older live versions — but the CURRENT version must\n * never serve or accept them. Their pair is passed into the shared core\n * alongside the live ones (so the collision check below still sees them —\n * see `buildFieldKeyMap`'s doc comment), and then stripped from the returned\n * map via `droppedColumns`/`droppedLabels` — which also drive `remap`:\n * without that, an unmapped retained column would pass through `remap`\n * unchanged and reach the response under its raw column name.\n */\nexport function createFieldKeyMap(fields: ParsedField[]): FieldKeyMap {\n const droppedColumns = new Set<string>()\n const droppedLabels = new Set<string>()\n const pairs: Array<{ label: string; column: string }> = []\n\n for (const f of fields) {\n if (!isColumnBacked(f)) continue\n // Keyed by `f.name`/`f.db_column.column_name` for every column-backed\n // field, live or tombstoned, so buildFieldKeyMap's post-check stripping\n // can find and remove a tombstone's own entry. That leans on field names\n // being unique across live and tombstoned fields within one type — true\n // today (core rejects a duplicate name at parse time) but only\n // load-bearing here because this refactor now writes a tombstone's pair\n // into the shared map at all; the original single-function\n // implementation `continue`d past a tombstone before ever writing one,\n // so the collision was structurally impossible.\n pairs.push({ label: f.name, column: f.db_column.column_name })\n if (f.removed === true) {\n // A tombstone's NAME refuses writes addressed to it; its COLUMN keeps\n // the retained data out of reads. Two sets, so neither can collide with\n // a live field's key from the other space.\n droppedLabels.add(f.name)\n droppedColumns.add(f.db_column.column_name)\n }\n }\n\n // `ownColumn` marks every column-backed field here too — a label IS the\n // field's name on this path — so the collision check skips it exactly as\n // it does for the projection variant below; only a paragraph, many-to-many\n // or programmatic field (no `ownColumn`) is ever inspected.\n return buildFieldKeyMap(\n pairs,\n fields.map((f) => ({\n label: f.name,\n // Conditional spread, not a plain assignment — exactOptionalPropertyTypes\n // forbids present-and-undefined.\n ...(isColumnBacked(f) && { ownColumn: f.db_column.column_name }),\n })),\n droppedColumns,\n droppedLabels\n )\n}\n\n/**\n * From a version's projection. The projection is already the label↔column\n * mapping and already excludes tombstones and non-column-backed fields, so\n * there is nothing to filter — but the collision check still needs every\n * field's label, which is why `allFields` is required.\n */\nexport function createFieldKeyMapFromProjection(\n projectionType: { fields: Array<{ column_name: string; exposed_as: string; fallback?: unknown }> },\n allFields: ParsedField[]\n): FieldKeyMap {\n const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }))\n const projectedColumns = new Set(pairs.map((p) => p.column))\n const projectedLabels = new Set(pairs.map((p) => p.label))\n // A column this version does not expose is dropped from READS; a name it\n // does not expose is dropped from WRITES. Computed independently: with one\n // shared set, un-dropping a projected label could un-drop an unrelated\n // field's column of the same text — which is exactly #2.\n const droppedColumns = new Set<string>()\n const droppedLabels = new Set<string>()\n for (const f of allFields) {\n if (!isColumnBacked(f)) continue\n if (!projectedColumns.has(f.db_column.column_name)) droppedColumns.add(f.db_column.column_name)\n if (!projectedLabels.has(f.name)) droppedLabels.add(f.name)\n }\n\n return buildFieldKeyMap(\n pairs,\n allFields.map((f) => ({\n label: f.name,\n // See buildFieldKeyMap's collision check: `ownColumn` marks every\n // column-backed field so the check skips it entirely — only a\n // paragraph/junction/programmatic label (no column of its own) can\n // still collide.\n ...(isColumnBacked(f) && { ownColumn: f.db_column.column_name }),\n })),\n droppedColumns,\n droppedLabels\n )\n}\n","import type { ParsedField, SchemaRegistry, VersionProjection } from '@bobbykim/manguito-cms-core'\nimport { isColumnBacked } from '../field-keys.js'\n\n// ─── One live version's view of the schema ────────────────────────────────────\n//\n// A VersionProjection says WHICH columns a version exposes, under WHICH labels,\n// and whether each is required. It deliberately says nothing about a field's\n// type: `field_type` and `ui_component` are recovered from CURRENT's field for\n// the same column instead.\n//\n// That join is sound, not convenient. `VERSION_COLUMN_MISSING` guarantees every\n// column a live version exposes still exists in current, and\n// `FIELD_TYPE_CHANGED_WHILE_LIVE` guarantees its type has not moved under it —\n// so current's field is a faithful source for everything except the label and\n// the requiredness, which the projection carries precisely because they DO\n// move. See the 2e design, \"Deriving a version's types\".\n\nexport type ViewField = {\n /** Current's field for this column — the source of `field_type` and `ui_component`. */\n field: ParsedField\n /** The name THIS version exposes the column under. */\n exposedAs: string\n /** THIS version's requiredness. Never current's — see VersionProjection. */\n required: boolean\n /**\n * The value to serve when the column holds null, for a column that stopped\n * being written. REST substitutes this in `projectRow`; GraphQL has no\n * route-level projection, so its scalar resolver has to do it or the two\n * surfaces disagree about the same version's contract.\n */\n fallback?: unknown\n /** Present only when this version's contract differs from current's. */\n deprecationReason?: string\n}\n\n/** Keyed by machine name, for content, taxonomy AND paragraph types. */\nexport type VersionView = Record<string, { fields: ViewField[] }>\n\n/** A type's column-backed fields keyed by column. Tombstones included — they hold a real column. */\nfunction byColumn(fields: ParsedField[]): Map<string, ParsedField> {\n const out = new Map<string, ParsedField>()\n for (const f of fields) {\n if (!isColumnBacked(f)) continue\n out.set(f.db_column.column_name, f)\n }\n return out\n}\n\n/**\n * Why this version's contract differs from current's, or undefined when it\n * does not. Keyed by COLUMN, because the label is exactly what may have moved.\n *\n * The reason names CURRENT, not the intermediate version where the change\n * happened: pinpointing that would mean walking every snapshot in between, and\n * \"what it is called now\" is the actionable fact for someone deciding whether\n * to upgrade.\n */\nfunction deprecationFor(\n field: { column_name: string; exposed_as: string },\n currentLabels: Map<string, string>,\n currentVersion: string\n): string | undefined {\n const currentLabel = currentLabels.get(field.column_name)\n if (currentLabel === undefined) {\n return `Removed in ${currentVersion}; column retained while this version is live.`\n }\n if (currentLabel !== field.exposed_as) {\n return `Renamed to '${currentLabel}' in ${currentVersion}.`\n }\n return undefined\n}\n\n/**\n * `projection` undefined means this version has no projection for a type — or\n * none at all. Those types fall back to the registry's own fields minus\n * tombstones, which is the inherited 2d boundary: paragraph types are never\n * projected, so nested paragraph content follows current's shape on every\n * version, by design.\n */\nexport function buildVersionView(input: {\n registry: SchemaRegistry\n projection: VersionProjection | undefined\n currentProjection: VersionProjection | undefined\n currentVersion: string\n}): VersionView {\n const { registry, projection, currentProjection, currentVersion } = input\n const out: VersionView = {}\n\n const sources: Array<Record<string, { fields: ParsedField[] }>> = [\n registry.content_types,\n registry.taxonomy_types,\n registry.paragraph_types,\n ]\n\n for (const source of sources) {\n for (const [typeName, type] of Object.entries(source)) {\n const projectionType = projection?.types[typeName]\n\n // No projection for this type: follow current, minus tombstones.\n if (projectionType === undefined) {\n out[typeName] = {\n fields: type.fields\n .filter((f) => f.removed !== true)\n .map((f) => ({ field: f, exposedAs: f.name, required: f.required })),\n }\n continue\n }\n\n const columns = byColumn(type.fields)\n const currentLabels = new Map<string, string>()\n for (const f of currentProjection?.types[typeName]?.fields ?? []) {\n currentLabels.set(f.column_name, f.exposed_as)\n }\n\n const fields: ViewField[] = []\n\n for (const pf of projectionType.fields) {\n const field = columns.get(pf.column_name)\n // Unreachable on a model that loaded — VERSION_COLUMN_MISSING rejects\n // it. Skipped rather than thrown so a hand-built baked model cannot\n // take the whole schema down over one field.\n if (field === undefined) continue\n const reason = deprecationFor(pf, currentLabels, currentVersion)\n fields.push({\n field,\n exposedAs: pf.exposed_as,\n required: pf.required,\n ...(pf.fallback !== undefined && { fallback: pf.fallback }),\n ...(reason !== undefined && { deprecationReason: reason }),\n })\n }\n\n // Fields with no column of their own — paragraph, many-to-many,\n // programmatic — never appear in a projection, so they must be appended\n // from the registry or an older version's schema would silently lose\n // every relation and computed field. They follow current on every\n // version (the 2d boundary), hence no deprecation reason.\n for (const f of type.fields) {\n if (isColumnBacked(f) || f.removed === true) continue\n fields.push({ field: f, exposedAs: f.name, required: f.required })\n }\n\n out[typeName] = { fields }\n }\n }\n\n return out\n}\n","import DataLoader from 'dataloader'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type {\n SchemaRegistry,\n ParsedContentType,\n ParsedTaxonomyType,\n ParsedParagraphType,\n} from '@bobbykim/manguito-cms-core'\nimport { buildRelationsMap, resolveRelationField, type RelationDef } from '../relations.js'\n\nexport interface RelationLoaders {\n load(typeName: string, fieldName: string, parent: Record<string, unknown>): Promise<unknown>\n}\n\ntype ParentRow = Record<string, unknown>\n\n// One RelationLoaders instance per request. Loaders batch sibling parents per\n// (type, field); the shared cache dedupes target lookups across all loaders in\n// the request, mirroring the repository's resolveRows cache.\nexport function createRelationLoaders(\n db: DrizzlePostgresInstance,\n registry: SchemaRegistry\n): RelationLoaders {\n const relMaps = new Map<string, Record<string, RelationDef>>()\n const loaders = new Map<string, DataLoader<ParentRow, unknown>>()\n const cache = new Map<string, unknown>()\n\n function relationsFor(typeName: string): Record<string, RelationDef> {\n let m = relMaps.get(typeName)\n if (!m) {\n const type = (registry.content_types[typeName] ??\n registry.taxonomy_types[typeName] ??\n registry.paragraph_types[typeName]) as\n | ParsedContentType\n | ParsedTaxonomyType\n | ParsedParagraphType\n | undefined\n m = type ? buildRelationsMap(type.fields, registry) : {}\n relMaps.set(typeName, m)\n }\n return m\n }\n\n function loaderFor(typeName: string, fieldName: string, rel: RelationDef): DataLoader<ParentRow, unknown> {\n const key = `${typeName}:${fieldName}`\n let dl = loaders.get(key)\n if (!dl) {\n dl = new DataLoader<ParentRow, unknown>(\n async (parents) => {\n const rows = parents as ParentRow[]\n // GraphQL is public-only (see app.ts) — always filter relation targets\n // to published rows, mirroring the REST public repos' publishedRelations.\n await resolveRelationField(db, rows, fieldName, rel, cache, true)\n // Column-backed relations resolve in place under their storage\n // column. Paragraph and junction relations have no column, so they\n // resolve under the field's name.\n // Not named `key`: loaderFor already declares one, for the cache.\n const resultKey = rel.type === 'reference' || rel.type === 'media' ? rel.fk_column : fieldName\n return rows.map((r) => r[resultKey])\n },\n // Batch within a tick; do not memoize by parent identity (rows are mutated\n // and may recur across nesting levels).\n { cache: false }\n )\n loaders.set(key, dl)\n }\n return dl\n }\n\n return {\n async load(typeName, fieldName, parent) {\n const rel = relationsFor(typeName)[fieldName]\n if (!rel) return null\n return loaderFor(typeName, fieldName, rel).load(parent)\n },\n }\n}\n","import crypto from 'node:crypto'\nimport { sql, type SQL } from 'drizzle-orm'\nimport type { ParsedField, ParsedParagraphType, SchemaRegistry } from '@bobbykim/manguito-cms-core'\nimport { relationCardinality, type Cardinality } from '@bobbykim/manguito-cms-core'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type { MediaDelta } from './media-references.js'\n\n// ─── Relation persistence ─────────────────────────────────────────────────────\n//\n// Owns paragraph/junction/reference/media relation table SQL — both the write\n// side (persist/delete below) and the read side (descriptors + resolvers further\n// down). Each persist/delete returns the MediaDelta of media ids it gained and\n// lost (see media-references.ts), which the caller merges and reconciles once.\n// The repository's resolveRows() drives the read resolvers.\n\nfunction quoteIdent(name: string): string {\n if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`)\n return `\"${name}\"`\n}\n\n// Paragraph rows live on a child table with their own image/video/file columns\n// (e.g. a \"photo card\" block embeds an image) — these need the same reference-count\n// bookkeeping as top-level content fields, one level down.\nfunction paragraphMediaFieldNames(pType: ParsedParagraphType): Set<string> {\n return new Set(\n pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.name)\n )\n}\n\nasync function fetchParagraphMediaIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const mediaColumns = pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.db_column!.column_name)\n if (mediaColumns.length === 0) return []\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const cols = sql.join(mediaColumns.map((c) => sql.raw(quoteIdent(c))), sql`, `)\n const result = await db.execute(\n sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n\n const ids: string[] = []\n for (const row of result.rows as Record<string, unknown>[]) {\n for (const col of mediaColumns) {\n const v = row[col]\n if (typeof v === 'string' && v !== '') ids.push(v)\n }\n }\n return ids\n}\n\n// A paragraph type's own paragraph fields — the one level of nesting ADR\n// core/0005 permits. Empty without a registry, which is how callers that have no\n// nesting to persist opt out.\nfunction nestedParagraphFields(\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Array<{ fieldName: string; nType: ParsedParagraphType; field: ParsedField }> {\n if (!registry) return []\n const out: Array<{ fieldName: string; nType: ParsedParagraphType; field: ParsedField }> = []\n for (const f of pType.fields) {\n if (f.db_column !== null) continue\n const comp = f.ui_component as { component: string; ref?: string }\n if (comp.component !== 'paragraph-embed' || !comp.ref) continue\n const nType = registry.paragraph_types[comp.ref]\n if (nType) out.push({ fieldName: f.name, nType, field: f })\n }\n return out\n}\n\n// Ids of the paragraph rows currently stored for one field of one parent.\nasync function fetchParagraphRowIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const result = await db.execute(\n sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n return (result.rows as Record<string, unknown>[])\n .map((r) => r['id'])\n .filter((id): id is string => typeof id === 'string')\n}\n\n// Replaces every paragraph row for one field of one parent (delete + reinsert),\n// returning the media ids removed (old rows) and added (new rows).\n//\n// `registry` enables one level of paragraph nesting (ADR core/0005): a nested\n// paragraph field has no column on its parent's table, so without this its items\n// were silently dropped on write. Nested rows hang off the *paragraph* row's id,\n// and are torn down with it so the delete+reinsert cannot orphan them.\nexport async function persistParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n parentType: string,\n fieldName: string,\n pType: ParsedParagraphType,\n items: unknown[],\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n const nested = nestedParagraphFields(pType, registry)\n\n // Tear down nested rows before their parents disappear, collecting the media\n // they referenced so reference counts stay correct.\n if (nested.length > 0) {\n const staleIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of staleIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n\n const mediaFieldNames = paragraphMediaFieldNames(pType)\n const added: string[] = []\n\n for (let i = 0; i < items.length; i++) {\n const pItem = items[i] as Record<string, unknown>\n const rowId = crypto.randomUUID()\n const data: Record<string, unknown> = {\n id: rowId,\n parent_id: itemId,\n parent_type: parentType,\n parent_field: fieldName,\n order: i,\n created_at: new Date(),\n updated_at: new Date(),\n }\n for (const pf of pType.fields) {\n if (!pf.db_column || pf.db_column.junction) continue\n data[pf.db_column.column_name] = pItem[pf.name] ?? null\n }\n const cols = sql.join(Object.keys(data).map((k) => sql.raw(quoteIdent(k))), sql`, `)\n const vals = sql.join(Object.values(data).map((v) => sql`${v}`), sql`, `)\n await db.execute(sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`)\n\n for (const name of mediaFieldNames) {\n const val = pItem[name]\n if (typeof val === 'string' && val !== '') added.push(val)\n }\n\n // Persist this row's nested paragraph items against the row just inserted.\n for (const n of nested) {\n const nestedItems = paragraphItems(n.field, pItem[n.fieldName])\n const d = await persistParagraphField(\n db,\n rowId,\n pType.db.table_name,\n n.fieldName,\n n.nType,\n nestedItems,\n registry\n )\n added.push(...d.added)\n removed.push(...d.removed)\n }\n }\n\n return { added, removed }\n}\n\n// Deletes all paragraph rows for one field of one parent (used when the parent is\n// being deleted — paragraph rows have no FK/cascade back to their parent, so they'd\n// leak otherwise). Returns the media those rows referenced, all removed.\nexport async function deleteParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n fieldName: string,\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n\n // Cascade to nested paragraph rows, which hang off these rows' ids.\n const nested = nestedParagraphFields(pType, registry)\n if (nested.length > 0) {\n const rowIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of rowIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n return { added: [], removed }\n}\n\n// Replaces an item's links for one many-to-many field. A repeated id is stored once, at its first\n// position; UUIDs compare case-insensitively, as Postgres stores them. With an\n// order column, each link keeps its position in the list (#57). All links go in\n// one INSERT: one round trip, however long the list.\nexport async function persistJunctionField(\n db: DrizzlePostgresInstance,\n itemId: string,\n junction: { table_name: string; left_column: string; right_column: string; order_column: boolean },\n relatedIds: string[]\n): Promise<void> {\n const tbl = sql.raw(quoteIdent(junction.table_name))\n const left = sql.raw(quoteIdent(junction.left_column))\n const right = sql.raw(quoteIdent(junction.right_column))\n const seen = new Set<string>()\n const ids = relatedIds.filter((id) => {\n const key = id.toLowerCase()\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n await db.execute(sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`)\n if (ids.length === 0) return\n const rows = sql.join(\n ids.map((rid, position) =>\n junction.order_column ? sql`(${itemId}, ${rid}, ${position})` : sql`(${itemId}, ${rid})`\n ),\n sql`, `\n )\n await db.execute(\n junction.order_column\n ? sql`INSERT INTO ${tbl} (${left}, ${right}, \"order\") VALUES ${rows}`\n : sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows}`\n )\n}\n\n// How every read lists an item's links: by the position saved with each link,\n// then by the linked id. The id breaks ties between links saved before\n// positions were (all 0), and orders a table without an order column, so a\n// list reads the same way every time.\nexport function junctionOrderBy(junction: { right_column: string; order_column: boolean }): SQL {\n const right = sql.raw(quoteIdent(junction.right_column))\n return junction.order_column ? sql` ORDER BY \"order\" ASC, ${right} ASC` : sql` ORDER BY ${right} ASC`\n}\n\n// ─── Relation descriptors and resolution (read side) ──────────────────────────\n//\n// Relation read resolution. These were lifted out of the repository so that all\n// paragraph/junction/reference/media table SQL lives in one module alongside the\n// write persistence above. The repository's resolveRows() calls into here.\n\nexport type ParagraphRelationDef = {\n type: 'paragraph'\n table: string\n // 'one' reads as the single row (or null); 'many' as the ordered list.\n cardinality: Cardinality\n}\n\nexport type ReferenceRelationDef = {\n type: 'reference'\n table: string\n fk_column: string\n}\n\nexport type JunctionRelationDef = {\n type: 'junction'\n table: string\n junction_table: string\n left_column: string\n right_column: string\n // Only ordered junctions have an \"order\" column (db codegen emits it only when\n // order_column is true) — the resolver must not ORDER BY it otherwise.\n order_column: boolean\n}\n\nexport type MediaRelationDef = {\n type: 'media'\n fk_column: string\n}\n\nexport type RelationDef =\n | ParagraphRelationDef\n | ReferenceRelationDef\n | JunctionRelationDef\n | MediaRelationDef\n\n// A 'one' relation reads as its single row (or null); a 'many' relation as the\n// list. Rows arrive ordered by \"order\", so a 'one' field holding legacy extra\n// rows reads its lowest-order row.\nexport function oneOrMany<T>(cardinality: Cardinality, list: T[]): T | T[] | null {\n return cardinality === 'one' ? (list[0] ?? null) : list\n}\n\n// The rows persistParagraphField stores for one paragraph field's request value.\n// A 'one' field carries an object (or null), a 'many' field an array. Callers\n// validate shape first (checkRelationInput), so any other value means \"none\".\nexport function paragraphItems(field: ParsedField, value: unknown): unknown[] {\n if (relationCardinality(field) === 'one') {\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? [value] : []\n }\n return Array.isArray(value) ? value : []\n}\n\n// Derives a content type's relations map from its parsed fields, so paragraph/\n// reference/media fields are actually resolved instead of silently dropped.\n// Junction and foreign_key info is already on each field's db_column; paragraph\n// fields store their target table on the paragraph type, hence the registry.\nexport function buildRelationsMap(\n fields: ParsedField[],\n registry: SchemaRegistry\n): Record<string, RelationDef> {\n const relations: Record<string, RelationDef> = {}\n\n for (const field of fields) {\n if (field.field_type === 'image' || field.field_type === 'video' || field.field_type === 'file') {\n if (!field.db_column) continue\n relations[field.name] = { type: 'media', fk_column: field.db_column.column_name }\n } else if (field.field_type === 'paragraph') {\n const ref = (field.ui_component as { ref?: string }).ref\n const pType = ref ? registry.paragraph_types[ref] : undefined\n if (!pType) continue\n relations[field.name] = {\n type: 'paragraph',\n table: pType.db.table_name,\n cardinality: relationCardinality(field) ?? 'many',\n }\n } else if (field.field_type === 'reference') {\n if (!field.db_column) continue\n if (field.db_column.junction) {\n const j = field.db_column.junction\n relations[field.name] = {\n type: 'junction',\n table: j.right_table,\n junction_table: j.table_name,\n left_column: j.left_column,\n right_column: j.right_column,\n order_column: j.order_column,\n }\n } else if (field.db_column.foreign_key) {\n relations[field.name] = {\n type: 'reference',\n table: field.db_column.foreign_key.table,\n fk_column: field.db_column.column_name,\n }\n }\n }\n }\n\n return relations\n}\n\nfunction groupBy<T extends Record<string, unknown>>(\n items: T[],\n key: string\n): Record<string, T[]> {\n const result: Record<string, T[]> = {}\n for (const item of items) {\n const k = String(item[key])\n if (!result[k]) result[k] = []\n result[k]!.push(item)\n }\n return result\n}\n\n// A reference/media pass resolves the FK IN PLACE: the resolved object replaces\n// the raw id under the same storage column. Nothing is deleted — a field whose\n// label differs from its storage column still keeps that column name; toLabels\n// applies the label downstream, after resolution.\n//\n// The same row object really does recur: reference/junction targets are cached by\n// `table:id`, so two parents can receive ONE shared object, and the GraphQL\n// dataloaders deliberately do not memoize by parent identity (\"rows are mutated and\n// may recur across nesting levels\", dataloaders.ts). So a row that already holds a\n// resolved object under the column is skipped, which makes the pass idempotent —\n// see needsFkResolution below.\nfunction needsFkResolution(row: Record<string, unknown>, fkColumn: string): boolean {\n // Resolved IN PLACE: the object sits under the column where the raw id was,\n // so an object (never a bare id) means this row is already done. A null\n // stays null on a second pass, which costs nothing — an empty FK list issues\n // no query.\n const current = row[fkColumn]\n return !(typeof current === 'object' && current !== null)\n}\n\n// Fully resolves a relation field on a batch of rows: paragraph → ordered rows,\n// reference → the target row, junction → target rows, media → the media row.\n// The cache (keyed table:id) dedupes target lookups within a request.\n//\n// `publishedOnly` (default false, preserving prior behavior for admin callers)\n// adds `AND published = true` to the reference/junction target-row SELECTs.\n// Reference and junction targets are always content or taxonomy tables, which\n// always carry a `published` boolean system field — see CONTENT_SYSTEM_FIELDS /\n// TAXONOMY_SYSTEM_FIELDS in packages/core/src/parser/parseSchema.ts. Paragraph\n// and media targets are deliberately excluded: paragraph rows are owned by\n// their parent (no independent publish state) and media rows have no\n// `published` column at all.\nexport async function resolveRelationField(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef,\n cache: Map<string, unknown>,\n publishedOnly = false\n): Promise<void> {\n if (rows.length === 0) return\n\n // Deduped by object identity: one batch can hold the same row object twice\n // (see needsFkResolution above). Resolving it twice would be harmless —\n // resolution is idempotent — but redundant, so this dedupe just avoids that\n // repeat work. The caller's array is left untouched — the GraphQL dataloader\n // maps over it to build its results.\n const batch = [...new Set(rows)]\n\n if (rel.type === 'paragraph') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n // Scoped by parent_field as well as parent_id: two paragraph fields of the\n // same paragraph type share one table, so filtering on the parent alone\n // hands each field the other's rows.\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of batch) {\n row[fieldName] = oneOrMany(rel.cardinality, byParent[row['id'] as string] ?? [])\n }\n } else if (rel.type === 'reference') {\n // Resolved IN PLACE, under the storage column. The column is a field's\n // internal identity, and toLabels applies whichever label the response\n // speaks. Writing under the field's name instead moved the value out of the\n // column, so any version labelling that column differently lost it.\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) row[rel.fk_column] = null\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`\n )\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[rel.fk_column] = fkVal ? (cache.get(`${rel.table}:${fkVal}`) ?? null) : null\n }\n } else if (rel.type === 'junction') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const junctionResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`\n )\n const jRows = junctionResult.rows as Record<string, unknown>[]\n const rightIds = [...new Set(jRows.map((r) => r[rel.right_column] as string))]\n const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const rightInList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const entitiesResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`\n )\n for (const item of entitiesResult.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n const byLeft = groupBy(jRows, rel.left_column)\n for (const row of batch) {\n const jEntries = byLeft[row['id'] as string] ?? []\n row[fieldName] = jEntries\n .map((jr) => cache.get(`${rel.table}:${jr[rel.right_column] as string}`))\n .filter(Boolean)\n }\n } else if (rel.type === 'media') {\n // Resolved IN PLACE, under the storage column — see the reference branch's\n // comment above.\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) row[rel.fk_column] = null\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`media:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(sql`SELECT * FROM \"media\" WHERE id IN (${inList})`)\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`media:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[rel.fk_column] = fkVal ? (cache.get(`media:${fkVal}`) ?? null) : null\n }\n }\n}\n\n// Populates paragraph/junction fields with bare ID arrays when they are not\n// explicitly included — they have no column on the owning table, so without this\n// they'd be silently absent. Reference fields already carry their bare ID as a\n// real column from `SELECT *`, so they need no extra query.\nexport async function resolveRelationBareIds(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef\n): Promise<void> {\n if (rows.length === 0) return\n\n if (rel.type === 'paragraph') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT id, parent_id FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of rows) {\n row[fieldName] = oneOrMany(rel.cardinality, (byParent[row['id'] as string] ?? []).map((r) => r['id']))\n }\n } else if (rel.type === 'junction') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT ${sql.raw(quoteIdent(rel.left_column))} AS left_id, ${sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`\n )\n const byLeft = groupBy(result.rows as Record<string, unknown>[], 'left_id')\n for (const row of rows) {\n row[fieldName] = (byLeft[row['id'] as string] ?? []).map((r) => r['right_id'])\n }\n }\n}\n","import { EnvelopArmor } from '@escape.tech/graphql-armor'\nimport { NoSchemaIntrospectionCustomRule } from 'graphql'\n\n// Depth + cost + alias + directive limits via GraphQL Armor. Returns { plugins } to\n// spread into Yoga's `plugins` array.\n//\n// Note: the installed `@escape.tech/graphql-armor` (3.2.0) exports `EnvelopArmor`, a\n// class whose `.protect()` returns `{ plugins }` — the standalone `EnvelopArmorPlugin`\n// export is a function that returns a single plugin, not a class with `.protect()`.\nexport function buildArmorPlugin(options: { maxDepth: number; maxComplexity: number }): {\n plugins: unknown[]\n} {\n const armor = new EnvelopArmor({\n maxDepth: { n: options.maxDepth },\n costLimit: { maxCost: options.maxComplexity },\n maxAliases: { n: 15 },\n maxDirectives: { n: 50 },\n blockFieldSuggestion: { enabled: true },\n // maxTokens is intentionally left at Armor's secure default (n=1000) to bound\n // GraphQL document token count at parse time, a layer the depth/cost/alias/directive\n // limits do not cover.\n })\n return armor.protect()\n}\n\n// Disables introspection (and the __schema/__type meta-fields) in production by\n// adding graphql's built-in validation rule when `enabled` is false.\nexport function introspectionPlugin(enabled: boolean): {\n onValidate(payload: { addValidationRule: (rule: unknown) => void }): void\n} {\n return {\n onValidate({ addValidationRule }) {\n if (!enabled) addValidationRule(NoSchemaIntrospectionCustomRule)\n },\n }\n}\n"],"mappings":"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","names":["import_graphql","import_graphql","import_graphql","import_graphql","import_manguito_cms_core","DataLoader","import_graphql"]}
|
|
1
|
+
{"version":3,"sources":["../../src/graphql/index.ts","../../src/graphql/handler.ts","../../src/graphql/schema.ts","../../src/graphql/scalars.ts","../../src/graphql/type-mapping.ts","../../src/graphql/naming.ts","../../src/graphql/filters.ts","../../src/graphql/resolvers.ts","../../src/field-keys.ts","../../src/graphql/version-view.ts","../../src/graphql/dataloaders.ts","../../src/relations.ts","../../src/relation-input.ts","../../src/graphql/security.ts"],"sourcesContent":["export { createGraphQLHandler } from './handler.js'\nexport type { ResolvedGraphQLOptions } from './handler.js'\n","import { createYoga, maskError } from 'graphql-yoga'\nimport type { Handler } from 'hono'\nimport type { SchemaRegistry, ContentRepository, VersionProjection } from '@bobbykim/manguito-cms-core'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type { ProgrammaticResolver } from '../programmatic/resolve.js'\nimport type { FieldKeyMap } from '../field-keys.js'\nimport type { GraphQLContext } from './context.js'\nimport { buildGraphQLSchema } from './schema.js'\nimport { buildVersionView, type VersionView } from './version-view.js'\nimport { createRelationLoaders } from './dataloaders.js'\nimport { buildArmorPlugin, introspectionPlugin } from './security.js'\n\nexport type ResolvedGraphQLOptions = {\n enabled: boolean\n maxDepth: number\n maxComplexity: number\n graphiql: boolean\n introspection: boolean\n}\n\n// Yoga decides whether an error is safe to show a client with\n// `error instanceof GraphQLError`. That check is realm-sensitive: GraphQL Armor\n// is CJS and throws a GraphQLError built from graphql's CJS entry, while Yoga is\n// ESM and compares against the class from graphql's ESM entry. Same version, two\n// classes — so `instanceof` fails and a depth/complexity rejection, which is the\n// client's own query being too large, was masked as\n// `INTERNAL_SERVER_ERROR: Unexpected error.` and logged server-side as a fault.\n//\n// Identify by name instead (what envelop's own masking does), walking the\n// originalError chain the same way. A GraphQLError with no originalError was\n// constructed deliberately by a validation rule or by us and carries no internals;\n// anything else — a resolver throwing, a driver error — still goes to Yoga's\n// masking untouched, dev-mode originalError detail included.\nfunction isClientSafeGraphQLError(error: unknown): boolean {\n let current = error as { name?: string; originalError?: unknown } | null | undefined\n while (current != null && typeof current === 'object' && current.name === 'GraphQLError') {\n if (current.originalError == null) return true\n current = current.originalError as typeof current\n }\n return false\n}\n\nexport function createGraphQLHandler(\n registry: SchemaRegistry,\n repos: Record<string, ContentRepository<unknown>>,\n fieldKeyMaps: Record<string, FieldKeyMap>,\n resolver: ProgrammaticResolver,\n db: DrizzlePostgresInstance,\n options: ResolvedGraphQLOptions,\n // The view is built HERE, not in app.ts: the `.` entry must keep no static\n // import of anything under graphql/ (ADR api/0006), so app.ts passes the\n // plain projection data it already imports as types from core.\n versioning: {\n endpoint?: string\n projection?: VersionProjection\n currentProjection?: VersionProjection\n currentVersion?: string\n // Current's field-key maps. A programmatic resolver reads its record in\n // current's labels on every version; omitted, it reads `fieldKeyMaps`.\n currentFieldKeyMaps?: Record<string, FieldKeyMap>\n } = {}\n): Handler {\n let view: VersionView | undefined\n if (versioning.projection !== undefined) {\n if (versioning.currentVersion === undefined) {\n throw new Error(\n 'createGraphQLHandler: currentVersion is required when a projection is supplied — ' +\n 'deprecation reasons name it, and an empty one ships \"Removed in .\" into published SDL.'\n )\n }\n view = buildVersionView({\n registry,\n projection: versioning.projection,\n currentProjection: versioning.currentProjection,\n currentVersion: versioning.currentVersion,\n })\n }\n const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps)\n const { plugins } = buildArmorPlugin({\n maxDepth: options.maxDepth,\n maxComplexity: options.maxComplexity,\n })\n\n const yoga = createYoga<Record<string, never>, GraphQLContext>({\n schema,\n graphqlEndpoint: versioning.endpoint ?? '/graphql',\n graphiql: options.graphiql,\n landingPage: false,\n maskedErrors: {\n maskError: (error, message, isDev) =>\n isClientSafeGraphQLError(error) ? (error as Error) : maskError(error, message, isDev),\n },\n // buildArmorPlugin's return type is `unknown[]` (see security.ts) since Armor's\n // plugin shapes aren't part of its public typings; Yoga's `plugins` option only\n // accepts its own `Plugin` type, so the cast bridges the two.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n plugins: [...(plugins as any[]), introspectionPlugin(options.introspection)],\n context: (): GraphQLContext => ({\n db,\n registry,\n repos,\n resolver,\n loaders: createRelationLoaders(db, registry),\n programmaticMemo: new WeakMap(),\n }),\n })\n\n return (c) => yoga.fetch(c.req.raw, {})\n}\n","import {\n GraphQLSchema,\n GraphQLObjectType,\n GraphQLEnumType,\n GraphQLNonNull,\n GraphQLList,\n GraphQLID,\n GraphQLString,\n GraphQLInt,\n GraphQLBoolean,\n type GraphQLFieldConfig,\n type GraphQLOutputType,\n type GraphQLFieldConfigMap,\n} from 'graphql'\nimport type {\n SchemaRegistry,\n ParsedContentType,\n ParsedTaxonomyType,\n ParsedParagraphType,\n ParsedField,\n ParsedEnumType,\n} from '@bobbykim/manguito-cms-core'\nimport { relationCardinality } from '@bobbykim/manguito-cms-core'\nimport type { GraphQLContext } from './context.js'\nimport type { FieldKeyMap } from '../field-keys.js'\nimport type { VersionView, ViewField } from './version-view.js'\nimport { DateTimeScalar } from './scalars.js'\nimport { scalarOutputType } from './type-mapping.js'\nimport {\n graphqlTypeName,\n singleQueryName,\n collectionQueryName,\n toCamelCase,\n isValidGraphQLName,\n buildFieldNameMap,\n} from './naming.js'\nimport { SortOrderEnum, buildSortFieldEnum, buildFilterInputType } from './filters.js'\nimport {\n scalarFieldResolver,\n relationFieldResolver,\n programmaticFieldResolver,\n collectionResolver,\n singleBySlugResolver,\n singletonResolver,\n taxonomySingleResolver,\n} from './resolvers.js'\n\nconst PAGE_META = new GraphQLObjectType({\n name: 'PageMeta',\n fields: {\n total: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.total },\n page: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.page },\n perPage: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.per_page },\n totalPages: { type: new GraphQLNonNull(GraphQLInt), resolve: (m) => m.total_pages },\n hasNext: { type: new GraphQLNonNull(GraphQLBoolean), resolve: (m) => m.has_next },\n hasPrev: { type: new GraphQLNonNull(GraphQLBoolean), resolve: (m) => m.has_prev },\n },\n})\n\nconst MEDIA = new GraphQLObjectType({\n name: 'Media',\n fields: {\n id: { type: new GraphQLNonNull(GraphQLID), resolve: (m) => m.id },\n type: { type: new GraphQLNonNull(GraphQLString), resolve: (m) => m.type },\n url: { type: new GraphQLNonNull(GraphQLString), resolve: (m) => m.url },\n mimeType: { type: new GraphQLNonNull(GraphQLString), resolve: (m) => m.mime_type },\n alt: { type: GraphQLString, resolve: (m) => m.alt },\n fileSize: { type: GraphQLInt, resolve: (m) => m.file_size },\n width: { type: GraphQLInt, resolve: (m) => m.width },\n height: { type: GraphQLInt, resolve: (m) => m.height },\n duration: { type: GraphQLInt, resolve: (m) => m.duration },\n },\n})\n\nexport function buildGraphQLSchema(\n registry: SchemaRegistry,\n fieldKeyMaps: Record<string, FieldKeyMap> = {},\n view?: VersionView,\n // The maps a programmatic resolver's record is built with: current's, on\n // every version. Defaults to `fieldKeyMaps`, which is right when they ARE\n // current's (no view, or the current version's own endpoint).\n programmaticKeyMaps: Record<string, FieldKeyMap> = fieldKeyMaps\n): GraphQLSchema {\n const objectTypes = new Map<string, GraphQLObjectType>() // machineName → type\n const enumTypes = new Map<string, GraphQLEnumType>() // enum machineName → type (only when valid)\n\n // Enum types (only when every value is a valid GraphQL identifier).\n for (const [name, enumType] of Object.entries(registry.enum_types) as [string, ParsedEnumType][]) {\n if (enumType.values.every(isValidGraphQLName)) {\n enumTypes.set(\n name,\n new GraphQLEnumType({\n name: graphqlTypeName(name),\n values: Object.fromEntries(enumType.values.map((v) => [v, { value: v }])),\n })\n )\n } else {\n process.stderr.write(\n `⚠ enum '${name}' has values that aren't valid GraphQL identifiers; exposing as String\\n`\n )\n }\n }\n\n // Output type for one field (scalar, enum, relation, media, programmatic).\n function outputTypeForField(field: ParsedField, required: boolean): GraphQLOutputType {\n const scalar = scalarOutputType(field.field_type)\n if (scalar) return required ? new GraphQLNonNull(scalar) : scalar\n\n if (field.field_type === 'enum') {\n const ref = field.ui_component.component === 'select' ? field.ui_component.enum_ref : undefined\n const et = ref ? enumTypes.get(ref) : undefined\n const t = et ?? GraphQLString\n return required ? new GraphQLNonNull(t) : t\n }\n\n if (field.field_type === 'image' || field.field_type === 'video' || field.field_type === 'file') {\n return MEDIA\n }\n\n if (field.field_type === 'paragraph') {\n const ref = field.ui_component.component === 'paragraph-embed' ? field.ui_component.ref : undefined\n const target = ref ? objectTypes.get(ref) : undefined\n if (ref && !target) {\n process.stderr.write(\n `⚠ paragraph field '${field.name}' targets unknown paragraph type '${ref}'; exposing as Media\\n`\n )\n }\n const t = (target ?? MEDIA) as GraphQLObjectType\n return relationCardinality(field) === 'one' ? t : new GraphQLList(new GraphQLNonNull(t))\n }\n\n if (field.field_type === 'reference') {\n const ref = field.ui_component.component === 'typeahead-select' ? field.ui_component.ref : undefined\n const target = ref ? objectTypes.get(ref) : undefined\n if (ref && !target) {\n process.stderr.write(`⚠ reference field '${field.name}' targets unknown type '${ref}'; exposing as Media\\n`)\n }\n const isMany = relationCardinality(field) === 'many'\n const t = (target ?? MEDIA) as GraphQLObjectType\n return isMany ? new GraphQLList(new GraphQLNonNull(t)) : t\n }\n\n return GraphQLString\n }\n\n /**\n * A type's fields minus its tombstones.\n *\n * A tombstone (`removed: true`) is column-backed — the parser keeps its\n * column alive for older live versions — but the version being served must\n * never expose it. REST drops it via the FieldKeyMap's drop-set, which\n * `remap` applies; GraphQL resolves per field by column through\n * `resolveFieldValue` and never touches that map, so the exclusion has to\n * happen where the schema is BUILT. A field absent from the schema is a\n * validation error, so the retained value cannot reach a response at all.\n */\n function exposedFields(type: { fields: ParsedField[] }): ParsedField[] {\n return type.fields.filter((f) => f.removed !== true)\n }\n\n /**\n * The fields one schema exposes for a type, as ViewFields. With a view, the\n * served version's own list; without one, the registry's minus tombstones.\n */\n function viewFieldsFor(machineName: string, type: { fields: ParsedField[] }): ViewField[] {\n return (\n view?.[machineName]?.fields ??\n exposedFields(type).map((f) => ({ field: f, exposedAs: f.name, required: f.required }))\n )\n }\n\n // Build the object type for a content/taxonomy/paragraph type. Fields are a\n // thunk so relations can reference types created later (circular graphs).\n function buildObjectType(\n machineName: string,\n type: ParsedContentType | ParsedTaxonomyType | ParsedParagraphType\n ): GraphQLObjectType {\n const visible = viewFieldsFor(machineName, type)\n\n // Every live media field the type has NOW, by registry name. That is the\n // key ctx.loaders.load looks a field up by. The set is current's, not this\n // version's visible one: the programmatic record speaks current's labels,\n // and REST resolves every media field in the registry for it too.\n // Tombstones are excluded; current's map drops their column regardless.\n const mediaFieldNames = type.fields\n .filter(\n (f) =>\n f.removed !== true &&\n (f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n )\n .map((f) => f.name)\n\n return new GraphQLObjectType({\n name: graphqlTypeName(machineName),\n fields: () => {\n const fields: GraphQLFieldConfigMap<Record<string, unknown>, GraphQLContext> = {}\n // System fields.\n fields['id'] = { type: new GraphQLNonNull(GraphQLID), resolve: (p) => p['id'] }\n if (type.schema_type !== 'paragraph-type') {\n fields['published'] = { type: new GraphQLNonNull(GraphQLBoolean), resolve: (p) => p['published'] }\n }\n if (type.schema_type === 'content-type') {\n fields['slug'] = { type: new GraphQLNonNull(GraphQLString), resolve: (p) => p['slug'] }\n }\n fields['createdAt'] = { type: new GraphQLNonNull(DateTimeScalar), resolve: (p) => p['created_at'] }\n fields['updatedAt'] = { type: new GraphQLNonNull(DateTimeScalar), resolve: (p) => p['updated_at'] }\n\n for (const vf of visible) {\n const field = vf.field\n const gqlName = toCamelCase(vf.exposedAs)\n const outType = outputTypeForField(field, vf.required)\n let resolve: GraphQLFieldConfig<Record<string, unknown>, GraphQLContext>['resolve']\n if (field.field_type === 'programmatic') {\n resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName])\n } else if (\n field.field_type === 'reference' ||\n field.field_type === 'paragraph' ||\n field.field_type === 'image' ||\n field.field_type === 'video' ||\n field.field_type === 'file'\n ) {\n resolve = relationFieldResolver(machineName, field.name)\n } else {\n resolve = scalarFieldResolver(field, vf.fallback)\n }\n fields[gqlName] = {\n type: outType,\n resolve,\n // Conditional spread, not `deprecationReason: vf.deprecationReason`\n // — exactOptionalPropertyTypes forbids present-and-undefined.\n ...(vf.deprecationReason !== undefined && { deprecationReason: vf.deprecationReason }),\n }\n }\n return fields\n },\n })\n }\n\n // Pass 1: create every object type (empty-safe thunks resolve later).\n // Track GraphQL type name → machine name so two schemas that collapse to the\n // same GraphQL type name (e.g. content--author / taxonomy--author both → \"Author\")\n // fail loud here instead of surfacing as a cryptic GraphQLSchema build error.\n const typeNameOwners = new Map<string, string>() // graphqlTypeName → machineName\n function registerTypeName(machineName: string): void {\n const gqlName = graphqlTypeName(machineName)\n const owner = typeNameOwners.get(gqlName)\n if (owner !== undefined && owner !== machineName) {\n throw new Error(\n `GraphQL type name collision: '${gqlName}' is produced by both '${owner}' and '${machineName}'. ` +\n `Rename one schema's machine name segment so their GraphQL type names differ.`\n )\n }\n typeNameOwners.set(gqlName, machineName)\n }\n\n for (const [name, ct] of Object.entries(registry.content_types)) {\n registerTypeName(name)\n objectTypes.set(name, buildObjectType(name, ct))\n }\n for (const [name, tt] of Object.entries(registry.taxonomy_types)) {\n registerTypeName(name)\n objectTypes.set(name, buildObjectType(name, tt))\n }\n for (const [name, pt] of Object.entries(registry.paragraph_types)) {\n registerTypeName(name)\n objectTypes.set(name, buildObjectType(name, pt))\n }\n\n // Pass 2: build the root Query type.\n const queryFields: GraphQLFieldConfigMap<unknown, GraphQLContext> = {}\n\n for (const [name, ct] of Object.entries(registry.content_types) as [string, ParsedContentType][]) {\n const objType = objectTypes.get(name)!\n const visible = viewFieldsFor(name, ct)\n // The version's own labels: translateFilters maps a GraphQL name back to a\n // label, and columnFor then takes that label to the storage column. System\n // fields go in too, because buildFilterInputType advertises createdAt and\n // updatedAt on every filter. Without them `createdAt` would fall through as\n // its own column name. columnFor returns nothing for a system field, so\n // translateFilters falls back to the name itself, which IS its column.\n const nameMap = buildFieldNameMap([\n ...visible.map((v) => v.exposedAs),\n ...ct.system_fields.map((s) => s.name),\n ])\n\n if (ct.only_one) {\n queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) }\n continue\n }\n\n const listType = new GraphQLObjectType({\n name: `${graphqlTypeName(name)}List`,\n fields: {\n data: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(objType))), resolve: (r) => r.data },\n meta: { type: new GraphQLNonNull(PAGE_META), resolve: (r) => r.meta },\n },\n })\n const filterType = buildFilterInputType(ct, visible)\n\n queryFields[collectionQueryName(name)] = {\n type: new GraphQLNonNull(listType),\n args: {\n page: { type: GraphQLInt },\n perPage: { type: GraphQLInt },\n sortBy: { type: buildSortFieldEnum(graphqlTypeName(name), fieldKeyMaps[name]) },\n sortOrder: { type: SortOrderEnum },\n ...(filterType ? { filter: { type: filterType } } : {}),\n },\n resolve: collectionResolver(name, nameMap, fieldKeyMaps[name]),\n }\n queryFields[singleQueryName(name)] = {\n type: objType,\n args: { slug: { type: new GraphQLNonNull(GraphQLString) } },\n resolve: singleBySlugResolver(name),\n }\n }\n\n for (const [name, tt] of Object.entries(registry.taxonomy_types) as [string, ParsedTaxonomyType][]) {\n const objType = objectTypes.get(name)!\n const listType = new GraphQLObjectType({\n name: `${graphqlTypeName(name)}List`,\n fields: {\n data: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(objType))), resolve: (r) => r.data },\n meta: { type: new GraphQLNonNull(PAGE_META), resolve: (r) => r.meta },\n },\n })\n queryFields[collectionQueryName(name)] = {\n type: new GraphQLNonNull(listType),\n args: { page: { type: GraphQLInt }, perPage: { type: GraphQLInt } },\n resolve: collectionResolver(\n name,\n buildFieldNameMap(viewFieldsFor(name, tt).map((v) => v.exposedAs)),\n fieldKeyMaps[name]\n ),\n }\n queryFields[singleQueryName(name)] = {\n type: objType,\n args: { id: { type: new GraphQLNonNull(GraphQLID) } },\n resolve: taxonomySingleResolver(name),\n }\n }\n\n return new GraphQLSchema({\n query: new GraphQLObjectType({ name: 'Query', fields: queryFields }),\n })\n}\n","import { GraphQLScalarType, Kind, valueFromASTUntyped } from 'graphql'\n\nexport const DateTimeScalar = new GraphQLScalarType({\n name: 'DateTime',\n description: 'ISO-8601 date-time string',\n serialize(value) {\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') return value\n if (typeof value === 'number') return new Date(value).toISOString()\n throw new TypeError(`DateTime cannot serialize value: ${String(value)}`)\n },\n parseValue(value) {\n if (typeof value !== 'string') throw new TypeError('DateTime must be a string')\n return value\n },\n parseLiteral(ast) {\n if (ast.kind !== Kind.STRING) throw new TypeError('DateTime must be a string')\n return ast.value\n },\n})\n\nexport const JSONScalar = new GraphQLScalarType({\n name: 'JSON',\n description: 'Arbitrary JSON value',\n serialize: (value) => value,\n parseValue: (value) => value,\n parseLiteral: (ast, variables) => valueFromASTUntyped(ast, variables ?? undefined),\n})\n","import {\n GraphQLString,\n GraphQLInt,\n GraphQLFloat,\n GraphQLBoolean,\n type GraphQLScalarType,\n} from 'graphql'\nimport type { FieldType } from '@bobbykim/manguito-cms-core'\nimport { DateTimeScalar, JSONScalar } from './scalars.js'\n\n// Scalar output type for a field. Relation/enum fields return null — schema.ts\n// resolves those against its type caches.\nexport function scalarOutputType(fieldType: FieldType): GraphQLScalarType | null {\n switch (fieldType) {\n case 'text/plain':\n case 'text/rich':\n return GraphQLString\n case 'integer':\n return GraphQLInt\n case 'float':\n return GraphQLFloat\n case 'boolean':\n return GraphQLBoolean\n case 'date':\n return DateTimeScalar\n case 'programmatic':\n return JSONScalar\n default:\n return null\n }\n}\n","// \"content--blog_post\" → \"blog_post\"; \"category\" → \"category\"\nexport function schemaSegment(machineName: string): string {\n const idx = machineName.indexOf('--')\n return idx === -1 ? machineName : machineName.slice(idx + 2)\n}\n\nfunction words(input: string): string[] {\n return input.split(/[_\\s-]+/).filter(Boolean)\n}\n\nexport function toPascalCase(input: string): string {\n return words(input)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join('')\n}\n\nexport function toCamelCase(input: string): string {\n const pascal = toPascalCase(input)\n return pascal.charAt(0).toLowerCase() + pascal.slice(1)\n}\n\n// Deterministic English pluralization for the common cases; irregular plurals\n// (person→people) are a known limitation — an optional per-type override can be\n// added later if one is wrong.\nexport function pluralize(word: string): string {\n if (/[^aeiou]y$/.test(word)) return word.slice(0, -1) + 'ies'\n if (/(s|x|z|ch|sh)$/.test(word)) return word + 'es'\n return word + 's'\n}\n\nconst GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/\nexport function isValidGraphQLName(name: string): boolean {\n return GRAPHQL_NAME.test(name)\n}\n\nexport function graphqlTypeName(machineName: string): string {\n return toPascalCase(schemaSegment(machineName))\n}\n\nexport function singleQueryName(machineName: string): string {\n return toCamelCase(schemaSegment(machineName))\n}\n\nexport function collectionQueryName(machineName: string): string {\n return pluralize(singleQueryName(machineName))\n}\n\nexport function buildFieldNameMap(schemaNames: string[]): {\n toGraphql(schemaName: string): string\n toSchema(graphqlName: string): string\n} {\n const toG = new Map<string, string>()\n const toS = new Map<string, string>()\n for (const name of schemaNames) {\n const g = toCamelCase(name)\n toG.set(name, g)\n toS.set(g, name)\n }\n return {\n toGraphql: (schemaName) => toG.get(schemaName) ?? toCamelCase(schemaName),\n toSchema: (graphqlName) => toS.get(graphqlName) ?? graphqlName,\n }\n}\n","import {\n GraphQLEnumType,\n GraphQLInputObjectType,\n GraphQLList,\n GraphQLNonNull,\n GraphQLString,\n GraphQLBoolean,\n GraphQLInt,\n GraphQLFloat,\n type GraphQLInputType,\n} from 'graphql'\nimport type {\n ParsedContentType,\n ParsedTaxonomyType,\n ParsedField,\n FilterValue,\n FilterOperator,\n FieldType,\n} from '@bobbykim/manguito-cms-core'\nimport { graphqlTypeName, buildFieldNameMap } from './naming.js'\nimport { scalarOutputType } from './type-mapping.js'\nimport { DateTimeScalar } from './scalars.js'\nimport type { FieldKeyMap } from '../field-keys.js'\nimport type { ViewField } from './version-view.js'\n\nexport const SortOrderEnum = new GraphQLEnumType({\n name: 'SortOrder',\n values: { ASC: { value: 'asc' }, DESC: { value: 'desc' } },\n})\n\n// Only these fields are sortable (mirrors the REST SORTABLE_FIELDS).\n//\n// `value` is the enum's internal value, and it is NOT uniformly a storage\n// column: `created_at` / `updated_at` are the snake_case forms of real system\n// columns, but `title` is a schema field's LABEL — the same value REST accepts\n// in `?sort_by=`. `collectionResolver` maps it to a column through the type's\n// FieldKeyMap before the repository builds its ORDER BY.\nconst SORTABLE: Array<{ gql: string; value: string }> = [\n { gql: 'title', value: 'title' },\n { gql: 'createdAt', value: 'created_at' },\n { gql: 'updatedAt', value: 'updated_at' },\n]\n\n/**\n * `fieldKeys` is THIS version's map. `title` in SORTABLE is a schema field's\n * LABEL, not a column, so a version that has no field with that label cannot\n * resolve it — and offering it anyway hands the repository a label that maps\n * to nothing, which surfaces as a 500 from SQL rather than a rejected query.\n * Mirrors `sortableColumnsFor` in versions.ts.\n *\n * `createdAt`/`updatedAt` are real system columns on every content type, so\n * they are always offered and the enum can never end up empty — which\n * GraphQL rejects.\n *\n * With no map, every value is kept: callers that have not threaded a\n * FieldKeyMap through behave exactly as before.\n */\nexport function buildSortFieldEnum(typeName: string, fieldKeys?: FieldKeyMap): GraphQLEnumType {\n const resolvable = SORTABLE.filter(\n (s) =>\n s.gql === 'createdAt' ||\n s.gql === 'updatedAt' ||\n fieldKeys === undefined ||\n fieldKeys.columnFor(s.value) !== undefined\n )\n return new GraphQLEnumType({\n name: `${typeName}SortField`,\n values: Object.fromEntries(resolvable.map((s) => [s.gql, { value: s.value }])),\n })\n}\n\n// A comparable filter input (eq/in/gt/gte/lt/lte) for a given scalar input type.\nfunction comparableFilterInput(name: string, scalar: GraphQLInputType): GraphQLInputObjectType {\n return new GraphQLInputObjectType({\n name,\n fields: {\n eq: { type: scalar },\n in: { type: new GraphQLList(new GraphQLNonNull(scalar)) },\n gt: { type: scalar },\n gte: { type: scalar },\n lt: { type: scalar },\n lte: { type: scalar },\n },\n })\n}\n\nfunction equalityFilterInput(name: string, scalar: GraphQLInputType): GraphQLInputObjectType {\n return new GraphQLInputObjectType({\n name,\n fields: {\n eq: { type: scalar },\n in: { type: new GraphQLList(new GraphQLNonNull(scalar)) },\n },\n })\n}\n\n// Shared per-scalar filter inputs (built once).\nconst StringFilter = equalityFilterInput('StringFilter', GraphQLString)\nconst BooleanFilter = new GraphQLInputObjectType({\n name: 'BooleanFilter',\n fields: { eq: { type: GraphQLBoolean } },\n})\nconst DateTimeFilter = comparableFilterInput('DateTimeFilter', DateTimeScalar)\nconst IDFilter = equalityFilterInput('IDFilter', GraphQLString)\nconst IntFilter = comparableFilterInput('IntFilter', GraphQLInt)\nconst FloatFilter = comparableFilterInput('FloatFilter', GraphQLFloat)\n\n// Choose a filter input for a field based on its scalar output type.\nfunction filterInputForField(fieldType: FieldType): GraphQLInputObjectType | null {\n const scalar = scalarOutputType(fieldType)\n if (fieldType === 'boolean') return BooleanFilter\n if (fieldType === 'date') return DateTimeFilter\n if (fieldType === 'integer') return IntFilter\n if (fieldType === 'float') return FloatFilter\n if (fieldType === 'reference') return IDFilter\n if (scalar === null) return null // only 'enum' reaches here — not filterable via a scalar input\n return StringFilter\n}\n\n// Build the <Type>Filter input. Programmatic fields are excluded (no column).\nexport function buildFilterInputType(\n type: ParsedContentType | ParsedTaxonomyType,\n viewFields?: ViewField[]\n): GraphQLInputObjectType | null {\n const fields: Record<string, { type: GraphQLInputObjectType }> = {}\n\n // System fields that are filterable columns.\n fields['id'] = { type: IDFilter }\n fields['published'] = { type: BooleanFilter }\n fields['createdAt'] = { type: DateTimeFilter }\n fields['updatedAt'] = { type: DateTimeFilter }\n if (type.schema_type === 'content-type') fields['slug'] = { type: StringFilter }\n\n // A view states the label THIS version exposes each field under; without\n // one, fall back to the field's own name minus tombstones (Task 2's rule).\n const entries: Array<{ field: ParsedField; exposedAs: string }> =\n viewFields !== undefined\n ? viewFields.map((v) => ({ field: v.field, exposedAs: v.exposedAs }))\n : type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name }))\n\n for (const { field: f, exposedAs } of entries) {\n if (f.field_type === 'programmatic' || f.field_type === 'paragraph') continue\n if (f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file') continue\n const input = filterInputForField(f.field_type)\n if (input) fields[buildFieldNameMap([exposedAs]).toGraphql(exposedAs)] = { type: input }\n }\n\n if (Object.keys(fields).length === 0) return null\n return new GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields })\n}\n\n// Convert a GraphQL filter argument into the repository's filters map, keyed by\n// snake_case column. eq → scalar, in → array (OR/IN), gt/gte/lt/lte → operator.\n//\n// `nameMap.toSchema` only ever recovers the schema field's LABEL (`field.name`)\n// from the GraphQL name — that mapping is unaware of storage columns. `columnFor`\n// (typically `FieldKeyMap.columnFor`) takes that label the rest of the way to the\n// storage column the repository actually filters on. It's optional so callers\n// that haven't threaded a FieldKeyMap through yet keep working unchanged: with no\n// mapper, or when it returns nothing for a given label, the label itself is used —\n// correct as long as it equals its column.\nexport function translateFilters(\n input: Record<string, unknown> | undefined,\n nameMap: { toSchema(g: string): string },\n columnFor?: (label: string) => string | undefined\n): Record<string, FilterValue> {\n const out: Record<string, FilterValue> = {}\n if (!input) return out\n\n for (const [gqlField, raw] of Object.entries(input)) {\n if (raw === null || typeof raw !== 'object') continue\n const label = nameMap.toSchema(gqlField)\n const column = columnFor?.(label) ?? label\n const spec = raw as Record<string, unknown>\n\n // eq/in take priority over range operators (gt/gte/lt/lte) when a client sends\n // both in one field filter — the range operators are silently dropped.\n if (spec['eq'] !== undefined) {\n out[column] = spec['eq'] as FilterValue\n } else if (Array.isArray(spec['in'])) {\n out[column] = spec['in'] as FilterValue\n } else {\n const op: FilterOperator = {}\n for (const k of ['gt', 'gte', 'lt', 'lte'] as const) {\n if (spec[k] !== undefined) op[k] = spec[k] as number | string\n }\n if (Object.keys(op).length > 0) out[column] = op\n }\n }\n return out\n}\n","import { GraphQLError } from 'graphql'\nimport type { ParsedField } from '@bobbykim/manguito-cms-core'\nimport type { GraphQLContext } from './context.js'\nimport { translateFilters } from './filters.js'\nimport { isColumnBacked, type FieldKeyMap } from '../field-keys.js'\n\ntype Row = Record<string, unknown>\n\nfunction codeError(code: string, message: string): GraphQLError {\n return new GraphQLError(message, { extensions: { code } })\n}\n\nfunction isPublished(item: Row | null): boolean {\n return !!item && item['published'] === true\n}\n\n/**\n * A GraphQL field's value on a content row. The GraphQL field NAME comes from\n * the field's label (see naming.ts), but the value lives under its storage\n * column, which differs once a field has been renamed.\n *\n * `fallback` is the value a version declares for a column that stopped being\n * written: rows created since the removal hold null there. Substituted on\n * null/undefined ONLY — never on '' or 0 — matching projectRow's rule, so the\n * REST and GraphQL surfaces cannot disagree about the same version.\n */\nexport function resolveFieldValue(\n field: ParsedField,\n row: Record<string, unknown>,\n fallback?: unknown\n): unknown {\n const key = isColumnBacked(field) ? field.db_column.column_name : field.name\n const value = row[key]\n if ((value === null || value === undefined) && fallback !== undefined) return fallback\n return value ?? null\n}\n\nexport function scalarFieldResolver(field: ParsedField, fallback?: unknown) {\n return (parent: Row): unknown => resolveFieldValue(field, parent, fallback)\n}\n\nexport function relationFieldResolver(typeName: string, schemaFieldName: string) {\n return (parent: Row, _args: unknown, ctx: GraphQLContext): Promise<unknown> =>\n ctx.loaders.load(typeName, schemaFieldName, parent)\n}\n\nexport function programmaticFieldResolver(\n typeName: string,\n schemaFieldName: string,\n mediaFieldNames: readonly string[] = [],\n fieldKeys?: FieldKeyMap\n) {\n return async (parent: Row, _args: unknown, ctx: GraphQLContext): Promise<unknown> => {\n let p = ctx.programmaticMemo.get(parent)\n if (!p) {\n p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys)\n ctx.programmaticMemo.set(parent, p)\n }\n return (await p)[schemaFieldName]\n }\n}\n\n// A programmatic resolver reads its record through `ctx.get()`, so that record\n// must look the same on both public surfaces. REST resolves media fields to full\n// objects before running resolvers; GraphQL reads through repositories that\n// resolve nothing, so without this `ctx.get('hero').url` would work over REST and\n// silently fall back to null over GraphQL.\n//\n// Media is resolved into a COPY, never the row itself: the programmatic record is\n// built from a copy so that projecting it to current's labels (toLabels below)\n// never touches the parent row, which the field resolvers still read by column.\n// The dataloaders resolve relations in place and idempotently now, so handing the\n// parent itself back to the loader would no longer break anything either — the\n// copy is kept to keep this path from mutating the parent, and it is harmless:\n// loaders still batch across the copy, so this costs no extra query.\n//\n// `ctx.get(fieldName)` takes the schema field name (the LABEL), so the record is\n// projected to CURRENT's labels (the caller passes current's map on every\n// version) before the resolver runs — the same order REST uses (see \"Response\n// projection order\" in routes/content.ts). GraphQL has no route-level\n// projection to double up with: every other field resolves per field by column\n// (resolveFieldValue), so this copy is the only mapped row, and it is mapped\n// strictly after the media loaders have run.\nasync function resolveProgrammaticRow(\n typeName: string,\n parent: Row,\n ctx: GraphQLContext,\n mediaFieldNames: readonly string[],\n fieldKeys?: FieldKeyMap\n): Promise<Record<string, unknown>> {\n const toLabels = (row: Row): Row => (fieldKeys ? fieldKeys.toLabels(row) : row)\n\n if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent))\n\n const enriched: Row = { ...parent }\n await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)))\n // The loaders resolve each media object IN PLACE under its storage column,\n // so toLabels carries it to its label like any scalar.\n return ctx.resolver.resolveItem(typeName, toLabels(enriched))\n}\n\ntype CollectionArgs = {\n page?: number\n perPage?: number\n /**\n * The sort enum's internal value (filters.ts `SORTABLE`), which mixes two key\n * spaces: `created_at` / `updated_at` are system COLUMNS, but `title` is a\n * schema field's LABEL. It must be mapped before it reaches the repository.\n */\n sortBy?: string\n sortOrder?: 'asc' | 'desc'\n filter?: Record<string, unknown>\n}\n\nexport function collectionResolver(\n typeName: string,\n nameMap: { toSchema(g: string): string },\n fieldKeys?: FieldKeyMap\n) {\n return async (_root: unknown, args: CollectionArgs, ctx: GraphQLContext) => {\n const page = args.page ?? 1\n const perPage = args.perPage ?? 10\n if (!Number.isInteger(page) || page < 1) {\n throw codeError('INVALID_PAGINATION', 'page must be ≥ 1')\n }\n if (!Number.isInteger(perPage) || perPage < 1 || perPage > 100) {\n throw codeError('INVALID_PAGINATION', 'perPage must be between 1 and 100')\n }\n const repo = ctx.repos[typeName]!\n // `ORDER BY` takes a storage column, never a label. `columnFor` resolves a\n // schema field's label to its column; a system column (`created_at`,\n // `updated_at`) is not a label at all, so it has no entry and falls\n // through unchanged. Identity today, since every label equals its column.\n const sortBy = args.sortBy ?? 'created_at'\n const sortColumn = fieldKeys?.columnFor(sortBy) ?? sortBy\n const result = await repo.findMany({\n published_only: true,\n page,\n per_page: perPage,\n sort_by: sortColumn as 'title' | 'created_at' | 'updated_at',\n sort_order: args.sortOrder ?? 'asc',\n filters: translateFilters(args.filter, nameMap, fieldKeys?.columnFor),\n })\n return { data: result.data as Row[], meta: result.meta }\n }\n}\n\nexport function singleBySlugResolver(typeName: string) {\n return async (_root: unknown, args: { slug: string }, ctx: GraphQLContext): Promise<Row | null> => {\n const repo = ctx.repos[typeName]!\n const item = (await repo.findBySlug(args.slug)) as Row | null\n return isPublished(item) ? item : null\n }\n}\n\nexport function singletonResolver(typeName: string) {\n return async (_root: unknown, _args: unknown, ctx: GraphQLContext): Promise<Row | null> => {\n const repo = ctx.repos[typeName]!\n const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 })\n return (result.data[0] as Row | undefined) ?? null\n }\n}\n\nexport function taxonomySingleResolver(typeName: string) {\n return async (_root: unknown, args: { id: string }, ctx: GraphQLContext): Promise<Row | null> => {\n const repo = ctx.repos[typeName]!\n const item = (await repo.findOne(args.id)) as Row | null\n return isPublished(item) ? item : null\n }\n}\n","import type { DbColumn, ParsedField } from '@bobbykim/manguito-cms-core'\n\n// ─── Label ↔ storage key mapping ──────────────────────────────────────────────\n//\n// A field has two names: its public LABEL (`field.name`, what API consumers and\n// the admin panel see) and its STORAGE key (`db_column.column_name`, the actual\n// Postgres column). They are identical today, but schema versioning makes a\n// rename change only the label — the column keeps its original name for the life\n// of the data. Every place that reads or writes a DB row must therefore use the\n// storage key, and every place that reads a request body or writes a response\n// must use the label.\n//\n// Only column-backed fields participate. Paragraph fields have no column\n// (their association lives on the paragraph table via parent_field) and\n// many-to-many references have no column either (the junction table owns the\n// association), so both keep the field name as their identity and are excluded.\n\n// A type predicate, not a plain boolean: narrowing `db_column` to non-null lets\n// the compiler enforce the invariant at every call site instead of each one\n// re-asserting it with `db_column!`.\nexport function isColumnBacked(field: ParsedField): field is ParsedField & { db_column: DbColumn } {\n const col = field.db_column\n if (col === null) return false\n if (col.column_name === '') return false\n if (col.junction) return false\n return true\n}\n\nexport type FieldKeyMap = {\n /** Request body (label-keyed) → storage-keyed. Unknown keys pass through. */\n toStorage(input: Record<string, unknown>): Record<string, unknown>\n /** DB row (storage-keyed) → label-keyed. Unknown keys pass through. */\n toLabels(row: Record<string, unknown>): Record<string, unknown>\n columnFor(label: string): string | undefined\n labelFor(column: string): string | undefined\n /** Labels of column-backed fields — the valid filter/sort surface. */\n labels: string[]\n /** False when every label equals its column, letting callers skip the copy. */\n diverges: boolean\n}\n\n/**\n * One field's place in the label space: the label it is known by, plus the\n * storage column it owns. `ownColumn` is ABSENT — never present-and-undefined,\n * per exactOptionalPropertyTypes — for a field with no column of its own.\n */\ntype LabelEntry = { label: string; ownColumn?: string }\n\n/**\n * The shared core: given a label↔column mapping and the FULL label space to\n * check against, produce a FieldKeyMap. Both `createFieldKeyMap` and\n * `createFieldKeyMapFromProjection` are thin adapters over this — the\n * collision check, the `diverges` computation and `remap` live here exactly\n * once.\n *\n * `pairs` may include entries that must NOT survive into the final map —\n * `createFieldKeyMap` passes tombstoned columns in alongside live ones,\n * exactly as the original single-function implementation did, so that the\n * collision check below (which runs against the unfiltered map) still\n * catches a paragraph/junction/programmatic field's label colliding with a\n * TOMBSTONE's column. Any pair whose label appears in `droppedLabels`, or\n * whose column appears in `droppedColumns`, is stripped from\n * `labelToColumn`/`columnToLabel` respectively, immediately after the check,\n * before `diverges` and `labels` are computed — so the caller does not need\n * to pre-filter `pairs` itself, only tell this function what to drop in each\n * key space.\n *\n * `allLabels` is every field's label, including fields with no column of\n * their own. That is not tidiness: a paragraph, many-to-many or programmatic\n * field's label is written into the same key space as storage columns before\n * `toLabels` runs, so one named after another field's column would overwrite\n * that column's value and then be renamed onto the other field's label.\n * Both constructors must pass the complete set.\n *\n * Each entry carries `ownColumn` — the column that field itself is backed by,\n * absent for a paragraph, many-to-many or programmatic field. The collision\n * check below needs it to tell which fields have no column of their own:\n * only those can collide, since a column-backed field's name is never a row\n * key.\n *\n * `droppedColumns`/`droppedLabels` also drive `remap`: each must actively\n * remove a key from a mapped object rather than merely leave it unmapped,\n * because `remap` passes an unmapped key through unchanged — a\n * retained-but-unexposed column would otherwise reach the output under its\n * raw name. They are kept as two SEPARATE sets, not one shared set, because\n * `toLabels` reads column-keyed input and `toStorage` reads label-keyed\n * input: a label and a column can be the same text, and one shared set would\n * let dropping one strip the other's unrelated pair.\n */\nfunction buildFieldKeyMap(\n pairs: Array<{ label: string; column: string }>,\n allLabels: LabelEntry[],\n droppedColumns: Set<string>,\n droppedLabels: Set<string>\n): FieldKeyMap {\n const labelToColumn = new Map<string, string>()\n const columnToLabel = new Map<string, string>()\n for (const { label, column } of pairs) {\n labelToColumn.set(label, column)\n columnToLabel.set(column, label)\n }\n\n // A label that is also some OTHER field's column name would make toLabels\n // ambiguous: two source keys would map onto one destination key.\n //\n // Checked against EVERY label in `allLabels`, not just the ones with a\n // pair of their own. Paragraph, many-to-many and programmatic fields have\n // no column of their own, but their labels are written into the same key\n // space as storage columns before toLabels runs (routes/admin/content.ts,\n // relations.ts, programmatic/resolve.ts). A paragraph field named after\n // another field's column would overwrite that column's value on the row\n // and then be renamed onto the other field's label — serving the wrong\n // value under the wrong key.\n //\n // Deliberately run BEFORE dropped pairs are stripped from `columnToLabel`\n // below: a paragraph/junction/programmatic field's label may collide with\n // a TOMBSTONE's column (the column still physically exists on the row),\n // and that must keep throwing. Stripping first would silently pass this\n // configuration. A column-backed field's label can no longer collide at\n // all (the skip below excludes it), live or tombstoned — only a field\n // with no column of its own is ever checked.\n //\n // Not detected: two fields declaring the SAME column name. A field can now\n // declare its own `column`, so this is no longer structurally impossible —\n // but core rejects it with `DUPLICATE_COLUMN` at parse time, so it is still\n // unreachable here.\n for (const { label, ownColumn } of allLabels) {\n // Only a field with no column of its own can collide. Its NAME is written\n // into the row as a key — paragraph, junction and programmatic values land\n // under it — so a name equal to some column would be relabelled as that\n // column's field. A column-backed field's name is never a row key: its\n // value lives under its column, and toLabels maps the column, not the name.\n if (ownColumn !== undefined) continue\n const columnOwner = columnToLabel.get(label)\n if (columnOwner !== undefined && columnOwner !== label) {\n throw new Error(\n `Fatal: field key map failed to build — field label \"${label}\" collides with the ` +\n `storage column of field \"${columnOwner}\". A field label may not reuse another ` +\n `field's column name. Rename the field, then run \\`manguito validate\\` to check your schema.`\n )\n }\n }\n\n // Only now strip dropped pairs — the collision check above has already run\n // against the full map. Each map is stripped by the set for ITS key space:\n // labelToColumn is keyed by label, columnToLabel by column. One shared set\n // let a label and a column of the same text strip each other's pair.\n for (const label of labelToColumn.keys()) {\n if (droppedLabels.has(label)) labelToColumn.delete(label)\n }\n for (const column of columnToLabel.keys()) {\n if (droppedColumns.has(column)) columnToLabel.delete(column)\n }\n\n const diverges = [...labelToColumn].some(([label, column]) => label !== column)\n\n function remap(\n input: Record<string, unknown>,\n lookup: Map<string, string>,\n dropped: Set<string>,\n droppedIfUnmapped?: Set<string>\n ): Record<string, unknown> {\n // Always returns a NEW object, even when nothing diverges. Returning `input`\n // unchanged would make aliasing depend on the schema, so a bug where a\n // caller mutates a row after mapping would reproduce only on renamed\n // fields. A shallow copy per row is not worth that class of bug.\n //\n // A key that IS a live label/column is mapped first, unconditionally —\n // `lookup` itself already excludes anything dropped (the strip loops\n // above), so a mapped hit can only be a live field. Only once a key is\n // found UNMAPPED do the drop sets apply, and `toStorage` checks both of\n // them there: `dropped` (droppedLabels — a tombstone's own name, refusing\n // a write addressed to it) and `droppedIfUnmapped` (droppedColumns — a\n // raw, unexposed column supplied directly, such as a tombstone's retained\n // column). Mapping first is what lets a live label write even when its\n // text happens to equal a tombstone's column (`description`/`d2` live,\n // `x`/`description` tombstone: `description` still maps to `d2`) while an\n // unmapped raw `description` would still be refused if it were itself a\n // dropped column. `toLabels` passes no `droppedIfUnmapped`, so it is\n // unaffected: a dropped column was already stripped from `lookup` and so\n // is always \"unmapped\", and is refused by `dropped` exactly as before.\n //\n // Unknown (and undropped) keys pass through unchanged.\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(input)) {\n const mapped = lookup.get(key)\n if (mapped !== undefined) {\n out[mapped] = input[key]\n continue\n }\n if (dropped.has(key) || droppedIfUnmapped?.has(key)) continue\n out[key] = input[key]\n }\n return out\n }\n\n return {\n // toStorage reads LABEL-keyed input: a live label maps and writes\n // regardless of either drop set; an unmapped key is refused if it is\n // EITHER the tombstone's own name (droppedLabels) or a raw, unexposed\n // column (droppedColumns) — see remap's comment.\n // toLabels reads COLUMN-keyed input, so it consults only droppedColumns.\n toStorage: (input) => remap(input, labelToColumn, droppedLabels, droppedColumns),\n toLabels: (row) => remap(row, columnToLabel, droppedColumns),\n columnFor: (label) => labelToColumn.get(label),\n labelFor: (column) => columnToLabel.get(column),\n labels: [...labelToColumn.keys()],\n diverges,\n }\n}\n\n/**\n * Built ONCE per content type at startup, not per request. Throws on a key\n * collision, matching how createCmsApp refuses to boot on a broken roles\n * registry: an ambiguous mapping would silently corrupt responses.\n *\n * Tombstones (`field.removed === true`) are column-backed — the parser keeps\n * their column alive for older live versions — but the CURRENT version must\n * never serve or accept them. Their pair is passed into the shared core\n * alongside the live ones (so the collision check below still sees them —\n * see `buildFieldKeyMap`'s doc comment), and then stripped from the returned\n * map via `droppedColumns`/`droppedLabels` — which also drive `remap`:\n * without that, an unmapped retained column would pass through `remap`\n * unchanged and reach the response under its raw column name.\n */\nexport function createFieldKeyMap(fields: ParsedField[]): FieldKeyMap {\n const droppedColumns = new Set<string>()\n const droppedLabels = new Set<string>()\n const pairs: Array<{ label: string; column: string }> = []\n\n for (const f of fields) {\n if (!isColumnBacked(f)) continue\n // Keyed by `f.name`/`f.db_column.column_name` for every column-backed\n // field, live or tombstoned, so buildFieldKeyMap's post-check stripping\n // can find and remove a tombstone's own entry. That leans on field names\n // being unique across live and tombstoned fields within one type — true\n // today (core rejects a duplicate name at parse time) but only\n // load-bearing here because this refactor now writes a tombstone's pair\n // into the shared map at all; the original single-function\n // implementation `continue`d past a tombstone before ever writing one,\n // so the collision was structurally impossible.\n pairs.push({ label: f.name, column: f.db_column.column_name })\n if (f.removed === true) {\n // A tombstone's NAME refuses writes addressed to it; its COLUMN keeps\n // the retained data out of reads. Two sets, so neither can collide with\n // a live field's key from the other space.\n droppedLabels.add(f.name)\n droppedColumns.add(f.db_column.column_name)\n }\n }\n\n // `ownColumn` marks every column-backed field here too — a label IS the\n // field's name on this path — so the collision check skips it exactly as\n // it does for the projection variant below; only a paragraph, many-to-many\n // or programmatic field (no `ownColumn`) is ever inspected.\n return buildFieldKeyMap(\n pairs,\n fields.map((f) => ({\n label: f.name,\n // Conditional spread, not a plain assignment — exactOptionalPropertyTypes\n // forbids present-and-undefined.\n ...(isColumnBacked(f) && { ownColumn: f.db_column.column_name }),\n })),\n droppedColumns,\n droppedLabels\n )\n}\n\n/**\n * From a version's projection. The projection is already the label↔column\n * mapping and already excludes tombstones and non-column-backed fields, so\n * there is nothing to filter — but the collision check still needs every\n * field's label, which is why `allFields` is required.\n */\nexport function createFieldKeyMapFromProjection(\n projectionType: { fields: Array<{ column_name: string; exposed_as: string; fallback?: unknown }> },\n allFields: ParsedField[]\n): FieldKeyMap {\n const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }))\n const projectedColumns = new Set(pairs.map((p) => p.column))\n const projectedLabels = new Set(pairs.map((p) => p.label))\n // A column this version does not expose is dropped from READS; a name it\n // does not expose is dropped from WRITES. Computed independently: with one\n // shared set, un-dropping a projected label could un-drop an unrelated\n // field's column of the same text — which is exactly #2.\n const droppedColumns = new Set<string>()\n const droppedLabels = new Set<string>()\n for (const f of allFields) {\n if (!isColumnBacked(f)) continue\n if (!projectedColumns.has(f.db_column.column_name)) droppedColumns.add(f.db_column.column_name)\n if (!projectedLabels.has(f.name)) droppedLabels.add(f.name)\n }\n\n return buildFieldKeyMap(\n pairs,\n allFields.map((f) => ({\n label: f.name,\n // See buildFieldKeyMap's collision check: `ownColumn` marks every\n // column-backed field so the check skips it entirely — only a\n // paragraph/junction/programmatic label (no column of its own) can\n // still collide.\n ...(isColumnBacked(f) && { ownColumn: f.db_column.column_name }),\n })),\n droppedColumns,\n droppedLabels\n )\n}\n","import type { ParsedField, SchemaRegistry, VersionProjection } from '@bobbykim/manguito-cms-core'\nimport { isColumnBacked } from '../field-keys.js'\n\n// ─── One live version's view of the schema ────────────────────────────────────\n//\n// A VersionProjection says WHICH columns a version exposes, under WHICH labels,\n// and whether each is required. It deliberately says nothing about a field's\n// type: `field_type` and `ui_component` are recovered from CURRENT's field for\n// the same column instead.\n//\n// That join is sound, not convenient. `VERSION_COLUMN_MISSING` guarantees every\n// column a live version exposes still exists in current, and\n// `FIELD_TYPE_CHANGED_WHILE_LIVE` guarantees its type has not moved under it —\n// so current's field is a faithful source for everything except the label and\n// the requiredness, which the projection carries precisely because they DO\n// move. See the 2e design, \"Deriving a version's types\".\n\nexport type ViewField = {\n /** Current's field for this column — the source of `field_type` and `ui_component`. */\n field: ParsedField\n /** The name THIS version exposes the column under. */\n exposedAs: string\n /** THIS version's requiredness. Never current's — see VersionProjection. */\n required: boolean\n /**\n * The value to serve when the column holds null, for a column that stopped\n * being written. REST substitutes this in `projectRow`; GraphQL has no\n * route-level projection, so its scalar resolver has to do it or the two\n * surfaces disagree about the same version's contract.\n */\n fallback?: unknown\n /** Present only when this version's contract differs from current's. */\n deprecationReason?: string\n}\n\n/** Keyed by machine name, for content, taxonomy AND paragraph types. */\nexport type VersionView = Record<string, { fields: ViewField[] }>\n\n/** A type's column-backed fields keyed by column. Tombstones included — they hold a real column. */\nfunction byColumn(fields: ParsedField[]): Map<string, ParsedField> {\n const out = new Map<string, ParsedField>()\n for (const f of fields) {\n if (!isColumnBacked(f)) continue\n out.set(f.db_column.column_name, f)\n }\n return out\n}\n\n/**\n * Why this version's contract differs from current's, or undefined when it\n * does not. Keyed by COLUMN, because the label is exactly what may have moved.\n *\n * The reason names CURRENT, not the intermediate version where the change\n * happened: pinpointing that would mean walking every snapshot in between, and\n * \"what it is called now\" is the actionable fact for someone deciding whether\n * to upgrade.\n */\nfunction deprecationFor(\n field: { column_name: string; exposed_as: string },\n currentLabels: Map<string, string>,\n currentVersion: string\n): string | undefined {\n const currentLabel = currentLabels.get(field.column_name)\n if (currentLabel === undefined) {\n return `Removed in ${currentVersion}; column retained while this version is live.`\n }\n if (currentLabel !== field.exposed_as) {\n return `Renamed to '${currentLabel}' in ${currentVersion}.`\n }\n return undefined\n}\n\n/**\n * `projection` undefined means this version has no projection for a type — or\n * none at all. Those types fall back to the registry's own fields minus\n * tombstones, which is the inherited 2d boundary: paragraph types are never\n * projected, so nested paragraph content follows current's shape on every\n * version, by design.\n */\nexport function buildVersionView(input: {\n registry: SchemaRegistry\n projection: VersionProjection | undefined\n currentProjection: VersionProjection | undefined\n currentVersion: string\n}): VersionView {\n const { registry, projection, currentProjection, currentVersion } = input\n const out: VersionView = {}\n\n const sources: Array<Record<string, { fields: ParsedField[] }>> = [\n registry.content_types,\n registry.taxonomy_types,\n registry.paragraph_types,\n ]\n\n for (const source of sources) {\n for (const [typeName, type] of Object.entries(source)) {\n const projectionType = projection?.types[typeName]\n\n // No projection for this type: follow current, minus tombstones.\n if (projectionType === undefined) {\n out[typeName] = {\n fields: type.fields\n .filter((f) => f.removed !== true)\n .map((f) => ({ field: f, exposedAs: f.name, required: f.required })),\n }\n continue\n }\n\n const columns = byColumn(type.fields)\n const currentLabels = new Map<string, string>()\n for (const f of currentProjection?.types[typeName]?.fields ?? []) {\n currentLabels.set(f.column_name, f.exposed_as)\n }\n\n const fields: ViewField[] = []\n\n for (const pf of projectionType.fields) {\n const field = columns.get(pf.column_name)\n // Unreachable on a model that loaded — VERSION_COLUMN_MISSING rejects\n // it. Skipped rather than thrown so a hand-built baked model cannot\n // take the whole schema down over one field.\n if (field === undefined) continue\n const reason = deprecationFor(pf, currentLabels, currentVersion)\n fields.push({\n field,\n exposedAs: pf.exposed_as,\n required: pf.required,\n ...(pf.fallback !== undefined && { fallback: pf.fallback }),\n ...(reason !== undefined && { deprecationReason: reason }),\n })\n }\n\n // Fields with no column of their own — paragraph, many-to-many,\n // programmatic — never appear in a projection, so they must be appended\n // from the registry or an older version's schema would silently lose\n // every relation and computed field. They follow current on every\n // version (the 2d boundary), hence no deprecation reason.\n for (const f of type.fields) {\n if (isColumnBacked(f) || f.removed === true) continue\n fields.push({ field: f, exposedAs: f.name, required: f.required })\n }\n\n out[typeName] = { fields }\n }\n }\n\n return out\n}\n","import DataLoader from 'dataloader'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type {\n SchemaRegistry,\n ParsedContentType,\n ParsedTaxonomyType,\n ParsedParagraphType,\n} from '@bobbykim/manguito-cms-core'\nimport { buildRelationsMap, resolveRelationField, type RelationDef } from '../relations.js'\n\nexport interface RelationLoaders {\n load(typeName: string, fieldName: string, parent: Record<string, unknown>): Promise<unknown>\n}\n\ntype ParentRow = Record<string, unknown>\n\n// One RelationLoaders instance per request. Loaders batch sibling parents per\n// (type, field); the shared cache dedupes target lookups across all loaders in\n// the request, mirroring the repository's resolveRows cache.\nexport function createRelationLoaders(\n db: DrizzlePostgresInstance,\n registry: SchemaRegistry\n): RelationLoaders {\n const relMaps = new Map<string, Record<string, RelationDef>>()\n const loaders = new Map<string, DataLoader<ParentRow, unknown>>()\n const cache = new Map<string, unknown>()\n\n function relationsFor(typeName: string): Record<string, RelationDef> {\n let m = relMaps.get(typeName)\n if (!m) {\n const type = (registry.content_types[typeName] ??\n registry.taxonomy_types[typeName] ??\n registry.paragraph_types[typeName]) as\n | ParsedContentType\n | ParsedTaxonomyType\n | ParsedParagraphType\n | undefined\n m = type ? buildRelationsMap(type.fields, registry) : {}\n relMaps.set(typeName, m)\n }\n return m\n }\n\n function loaderFor(typeName: string, fieldName: string, rel: RelationDef): DataLoader<ParentRow, unknown> {\n const key = `${typeName}:${fieldName}`\n let dl = loaders.get(key)\n if (!dl) {\n dl = new DataLoader<ParentRow, unknown>(\n async (parents) => {\n const rows = parents as ParentRow[]\n // GraphQL is public-only (see app.ts) — always filter relation targets\n // to published rows, mirroring the REST public repos' publishedRelations.\n await resolveRelationField(db, rows, fieldName, rel, cache, true)\n // Column-backed relations resolve in place under their storage\n // column. Paragraph and junction relations have no column, so they\n // resolve under the field's name.\n // Not named `key`: loaderFor already declares one, for the cache.\n const resultKey = rel.type === 'reference' || rel.type === 'media' ? rel.fk_column : fieldName\n return rows.map((r) => r[resultKey])\n },\n // Batch within a tick; do not memoize by parent identity (rows are mutated\n // and may recur across nesting levels).\n { cache: false }\n )\n loaders.set(key, dl)\n }\n return dl\n }\n\n return {\n async load(typeName, fieldName, parent) {\n const rel = relationsFor(typeName)[fieldName]\n if (!rel) return null\n return loaderFor(typeName, fieldName, rel).load(parent)\n },\n }\n}\n","import crypto from 'node:crypto'\nimport { sql, type SQL } from 'drizzle-orm'\nimport type { ParsedField, ParsedParagraphType, SchemaRegistry } from '@bobbykim/manguito-cms-core'\nimport { relationCardinality, type Cardinality } from '@bobbykim/manguito-cms-core'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type { MediaDelta } from './media-references.js'\nimport { paragraphTypeOf } from './relation-input.js'\n\n// ─── Relation persistence ─────────────────────────────────────────────────────\n//\n// Owns paragraph/junction/reference/media relation table SQL — both the write\n// side (persist/delete below) and the read side (descriptors + resolvers further\n// down). Each persist/delete returns the MediaDelta of media ids it gained and\n// lost (see media-references.ts), which the caller merges and reconciles once.\n// The repository's resolveRows() drives the read resolvers.\n\nfunction quoteIdent(name: string): string {\n if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`)\n return `\"${name}\"`\n}\n\n// Paragraph rows live on a child table with their own image/video/file columns\n// (e.g. a \"photo card\" block embeds an image) — these need the same reference-count\n// bookkeeping as top-level content fields, one level down.\nfunction paragraphMediaFieldNames(pType: ParsedParagraphType): Set<string> {\n return new Set(\n pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.name)\n )\n}\n\nasync function fetchParagraphMediaIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const mediaColumns = pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.db_column!.column_name)\n if (mediaColumns.length === 0) return []\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const cols = sql.join(mediaColumns.map((c) => sql.raw(quoteIdent(c))), sql`, `)\n const result = await db.execute(\n sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n\n const ids: string[] = []\n for (const row of result.rows as Record<string, unknown>[]) {\n for (const col of mediaColumns) {\n const v = row[col]\n if (typeof v === 'string' && v !== '') ids.push(v)\n }\n }\n return ids\n}\n\n// A paragraph type's own paragraph fields — the one level of nesting ADR\n// core/0005 permits. Empty without a registry, which is how callers that have no\n// nesting to persist opt out.\nfunction nestedParagraphFields(\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Array<{ fieldName: string; nType: ParsedParagraphType; field: ParsedField }> {\n if (!registry) return []\n const out: Array<{ fieldName: string; nType: ParsedParagraphType; field: ParsedField }> = []\n for (const f of pType.fields) {\n if (f.db_column !== null) continue\n const nType = paragraphTypeOf(f, registry)\n if (nType) out.push({ fieldName: f.name, nType, field: f })\n }\n return out\n}\n\n// Ids of the paragraph rows currently stored for one field of one parent.\nasync function fetchParagraphRowIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const result = await db.execute(\n sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n return (result.rows as Record<string, unknown>[])\n .map((r) => r['id'])\n .filter((id): id is string => typeof id === 'string')\n}\n\n// Ids of every paragraph row an item owns: the rows of its paragraph fields and,\n// one level down (ADR core/0005), the rows nested inside those. Rows of\n// different paragraph tables mix in one list; their UUIDs do not collide.\n// One query per paragraph field, and one per nested field covering all of that\n// field's parent rows at once.\nexport async function ownParagraphRowIds(\n db: DrizzlePostgresInstance,\n registry: SchemaRegistry,\n fields: ParsedField[],\n itemId: string\n): Promise<string[]> {\n const ids: string[] = []\n for (const f of fields) {\n const pType = paragraphTypeOf(f, registry)\n if (!pType) continue\n const rowIds = await fetchParagraphRowIds(db, pType, itemId, f.name)\n ids.push(...rowIds)\n if (rowIds.length === 0) continue\n for (const n of nestedParagraphFields(pType, registry)) {\n const result = await db.execute(\n sql`SELECT id FROM ${sql.raw(quoteIdent(n.nType.db.table_name))} WHERE parent_id = ANY(${sql.param(rowIds)}::uuid[]) AND parent_field = ${n.fieldName}`\n )\n for (const r of result.rows as Record<string, unknown>[]) {\n if (typeof r['id'] === 'string') ids.push(r['id'])\n }\n }\n }\n return ids\n}\n\n// Replaces every paragraph row for one field of one parent (delete + reinsert),\n// returning the media ids removed (old rows) and added (new rows).\n//\n// `registry` enables one level of paragraph nesting (ADR core/0005): a nested\n// paragraph field has no column on its parent's table, so without this its items\n// were silently dropped on write. Nested rows hang off the *paragraph* row's id,\n// and are torn down with it so the delete+reinsert cannot orphan them.\nexport async function persistParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n parentType: string,\n fieldName: string,\n pType: ParsedParagraphType,\n items: unknown[],\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n const nested = nestedParagraphFields(pType, registry)\n\n // Tear down nested rows before their parents disappear, collecting the media\n // they referenced so reference counts stay correct.\n if (nested.length > 0) {\n const staleIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of staleIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n\n const mediaFieldNames = paragraphMediaFieldNames(pType)\n const added: string[] = []\n\n for (let i = 0; i < items.length; i++) {\n const pItem = items[i] as Record<string, unknown>\n const rowId = crypto.randomUUID()\n const data: Record<string, unknown> = {\n id: rowId,\n parent_id: itemId,\n parent_type: parentType,\n parent_field: fieldName,\n order: i,\n created_at: new Date(),\n updated_at: new Date(),\n }\n for (const pf of pType.fields) {\n if (!pf.db_column || pf.db_column.junction) continue\n data[pf.db_column.column_name] = pItem[pf.name] ?? null\n }\n const cols = sql.join(Object.keys(data).map((k) => sql.raw(quoteIdent(k))), sql`, `)\n const vals = sql.join(Object.values(data).map((v) => sql`${v}`), sql`, `)\n await db.execute(sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`)\n\n for (const name of mediaFieldNames) {\n const val = pItem[name]\n if (typeof val === 'string' && val !== '') added.push(val)\n }\n\n // Persist this row's nested paragraph items against the row just inserted.\n for (const n of nested) {\n const nestedItems = paragraphItems(n.field, pItem[n.fieldName])\n const d = await persistParagraphField(\n db,\n rowId,\n pType.db.table_name,\n n.fieldName,\n n.nType,\n nestedItems,\n registry\n )\n added.push(...d.added)\n removed.push(...d.removed)\n }\n }\n\n return { added, removed }\n}\n\n// Deletes all paragraph rows for one field of one parent (used when the parent is\n// being deleted — paragraph rows have no FK/cascade back to their parent, so they'd\n// leak otherwise). Returns the media those rows referenced, all removed.\nexport async function deleteParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n fieldName: string,\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n\n // Cascade to nested paragraph rows, which hang off these rows' ids.\n const nested = nestedParagraphFields(pType, registry)\n if (nested.length > 0) {\n const rowIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of rowIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n return { added: [], removed }\n}\n\n// Replaces an item's links for one many-to-many field. A repeated id is stored once, at its first\n// position; UUIDs compare case-insensitively, as Postgres stores them. With an\n// order column, each link keeps its position in the list (#57). All links go in\n// one INSERT: one round trip, however long the list. Writes are not\n// transactional, so two overlapping saves of one item can run delete, delete,\n// insert, insert; ON CONFLICT DO NOTHING lets the second insert find the first's\n// links (the table's unique constraint) and store each link once.\nexport async function persistJunctionField(\n db: DrizzlePostgresInstance,\n itemId: string,\n junction: { table_name: string; left_column: string; right_column: string; order_column: boolean },\n relatedIds: string[]\n): Promise<void> {\n const tbl = sql.raw(quoteIdent(junction.table_name))\n const left = sql.raw(quoteIdent(junction.left_column))\n const right = sql.raw(quoteIdent(junction.right_column))\n const seen = new Set<string>()\n const ids = relatedIds.filter((id) => {\n const key = id.toLowerCase()\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n await db.execute(sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`)\n if (ids.length === 0) return\n const rows = sql.join(\n ids.map((rid, position) =>\n junction.order_column ? sql`(${itemId}, ${rid}, ${position})` : sql`(${itemId}, ${rid})`\n ),\n sql`, `\n )\n await db.execute(\n junction.order_column\n ? sql`INSERT INTO ${tbl} (${left}, ${right}, \"order\") VALUES ${rows} ON CONFLICT DO NOTHING`\n : sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows} ON CONFLICT DO NOTHING`\n )\n}\n\n// How every read lists an item's links: by the position saved with each link,\n// then by the linked id. The id breaks ties between links saved before\n// positions were (all 0), and orders a table without an order column, so a\n// list reads the same way every time.\nexport function junctionOrderBy(junction: { right_column: string; order_column: boolean }): SQL {\n const right = sql.raw(quoteIdent(junction.right_column))\n return junction.order_column ? sql` ORDER BY \"order\" ASC, ${right} ASC` : sql` ORDER BY ${right} ASC`\n}\n\n// Keeps each key's first item, in order. A link stored twice (rows written\n// before duplicates were dropped) then reads once, at its first position.\nexport function firstOfEach<T>(items: T[], key: (item: T) => string): T[] {\n const seen = new Set<string>()\n return items.filter((item) => {\n const k = key(item)\n if (seen.has(k)) return false\n seen.add(k)\n return true\n })\n}\n\n// ─── Relation descriptors and resolution (read side) ──────────────────────────\n//\n// Relation read resolution. These were lifted out of the repository so that all\n// paragraph/junction/reference/media table SQL lives in one module alongside the\n// write persistence above. The repository's resolveRows() calls into here.\n\nexport type ParagraphRelationDef = {\n type: 'paragraph'\n table: string\n // 'one' reads as the single row (or null); 'many' as the ordered list.\n cardinality: Cardinality\n}\n\nexport type ReferenceRelationDef = {\n type: 'reference'\n table: string\n fk_column: string\n}\n\nexport type JunctionRelationDef = {\n type: 'junction'\n table: string\n junction_table: string\n left_column: string\n right_column: string\n // Only ordered junctions have an \"order\" column (db codegen emits it only when\n // order_column is true) — the resolver must not ORDER BY it otherwise.\n order_column: boolean\n}\n\nexport type MediaRelationDef = {\n type: 'media'\n fk_column: string\n}\n\nexport type RelationDef =\n | ParagraphRelationDef\n | ReferenceRelationDef\n | JunctionRelationDef\n | MediaRelationDef\n\n// A 'one' relation reads as its single row (or null); a 'many' relation as the\n// list. Rows arrive ordered by \"order\", so a 'one' field holding legacy extra\n// rows reads its lowest-order row.\nexport function oneOrMany<T>(cardinality: Cardinality, list: T[]): T | T[] | null {\n return cardinality === 'one' ? (list[0] ?? null) : list\n}\n\n// The rows persistParagraphField stores for one paragraph field's request value.\n// A 'one' field carries an object (or null), a 'many' field an array. Callers\n// validate shape first (checkRelationInput), so any other value means \"none\".\nexport function paragraphItems(field: ParsedField, value: unknown): unknown[] {\n if (relationCardinality(field) === 'one') {\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? [value] : []\n }\n return Array.isArray(value) ? value : []\n}\n\n// Derives a content type's relations map from its parsed fields, so paragraph/\n// reference/media fields are actually resolved instead of silently dropped.\n// Junction and foreign_key info is already on each field's db_column; paragraph\n// fields store their target table on the paragraph type, hence the registry.\nexport function buildRelationsMap(\n fields: ParsedField[],\n registry: SchemaRegistry\n): Record<string, RelationDef> {\n const relations: Record<string, RelationDef> = {}\n\n for (const field of fields) {\n if (field.field_type === 'image' || field.field_type === 'video' || field.field_type === 'file') {\n if (!field.db_column) continue\n relations[field.name] = { type: 'media', fk_column: field.db_column.column_name }\n } else if (field.field_type === 'paragraph') {\n const ref = (field.ui_component as { ref?: string }).ref\n const pType = ref ? registry.paragraph_types[ref] : undefined\n if (!pType) continue\n relations[field.name] = {\n type: 'paragraph',\n table: pType.db.table_name,\n cardinality: relationCardinality(field) ?? 'many',\n }\n } else if (field.field_type === 'reference') {\n if (!field.db_column) continue\n if (field.db_column.junction) {\n const j = field.db_column.junction\n relations[field.name] = {\n type: 'junction',\n table: j.right_table,\n junction_table: j.table_name,\n left_column: j.left_column,\n right_column: j.right_column,\n order_column: j.order_column,\n }\n } else if (field.db_column.foreign_key) {\n relations[field.name] = {\n type: 'reference',\n table: field.db_column.foreign_key.table,\n fk_column: field.db_column.column_name,\n }\n }\n }\n }\n\n return relations\n}\n\nfunction groupBy<T extends Record<string, unknown>>(\n items: T[],\n key: string\n): Record<string, T[]> {\n const result: Record<string, T[]> = {}\n for (const item of items) {\n const k = String(item[key])\n if (!result[k]) result[k] = []\n result[k]!.push(item)\n }\n return result\n}\n\n// A reference/media pass resolves the FK IN PLACE: the resolved object replaces\n// the raw id under the same storage column. Nothing is deleted — a field whose\n// label differs from its storage column still keeps that column name; toLabels\n// applies the label downstream, after resolution.\n//\n// The same row object really does recur: reference/junction targets are cached by\n// `table:id`, so two parents can receive ONE shared object, and the GraphQL\n// dataloaders deliberately do not memoize by parent identity (\"rows are mutated and\n// may recur across nesting levels\", dataloaders.ts). So a row that already holds a\n// resolved object under the column is skipped, which makes the pass idempotent —\n// see needsFkResolution below.\nfunction needsFkResolution(row: Record<string, unknown>, fkColumn: string): boolean {\n // Resolved IN PLACE: the object sits under the column where the raw id was,\n // so an object (never a bare id) means this row is already done. A null\n // stays null on a second pass, which costs nothing — an empty FK list issues\n // no query.\n const current = row[fkColumn]\n return !(typeof current === 'object' && current !== null)\n}\n\n// Fully resolves a relation field on a batch of rows: paragraph → ordered rows,\n// reference → the target row, junction → target rows, media → the media row.\n// The cache (keyed table:id) dedupes target lookups within a request.\n//\n// `publishedOnly` (default false, preserving prior behavior for admin callers)\n// adds `AND published = true` to the reference/junction target-row SELECTs.\n// Reference and junction targets are always content or taxonomy tables, which\n// always carry a `published` boolean system field — see CONTENT_SYSTEM_FIELDS /\n// TAXONOMY_SYSTEM_FIELDS in packages/core/src/parser/parseSchema.ts. Paragraph\n// and media targets are deliberately excluded: paragraph rows are owned by\n// their parent (no independent publish state) and media rows have no\n// `published` column at all.\nexport async function resolveRelationField(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef,\n cache: Map<string, unknown>,\n publishedOnly = false\n): Promise<void> {\n if (rows.length === 0) return\n\n // Deduped by object identity: one batch can hold the same row object twice\n // (see needsFkResolution above). Resolving it twice would be harmless —\n // resolution is idempotent — but redundant, so this dedupe just avoids that\n // repeat work. The caller's array is left untouched — the GraphQL dataloader\n // maps over it to build its results.\n const batch = [...new Set(rows)]\n\n if (rel.type === 'paragraph') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n // Scoped by parent_field as well as parent_id: two paragraph fields of the\n // same paragraph type share one table, so filtering on the parent alone\n // hands each field the other's rows.\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC, id ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of batch) {\n row[fieldName] = oneOrMany(rel.cardinality, byParent[row['id'] as string] ?? [])\n }\n } else if (rel.type === 'reference') {\n // Resolved IN PLACE, under the storage column. The column is a field's\n // internal identity, and toLabels applies whichever label the response\n // speaks. Writing under the field's name instead moved the value out of the\n // column, so any version labelling that column differently lost it.\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) row[rel.fk_column] = null\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`\n )\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[rel.fk_column] = fkVal ? (cache.get(`${rel.table}:${fkVal}`) ?? null) : null\n }\n } else if (rel.type === 'junction') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const junctionResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`\n )\n const jRows = junctionResult.rows as Record<string, unknown>[]\n const rightIds = [...new Set(jRows.map((r) => r[rel.right_column] as string))]\n const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const rightInList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const entitiesResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`\n )\n for (const item of entitiesResult.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n const byLeft = groupBy(jRows, rel.left_column)\n for (const row of batch) {\n const jEntries = firstOfEach(byLeft[row['id'] as string] ?? [], (jr) => jr[rel.right_column] as string)\n row[fieldName] = jEntries\n .map((jr) => cache.get(`${rel.table}:${jr[rel.right_column] as string}`))\n .filter(Boolean)\n }\n } else if (rel.type === 'media') {\n // Resolved IN PLACE, under the storage column — see the reference branch's\n // comment above.\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) row[rel.fk_column] = null\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`media:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(sql`SELECT * FROM \"media\" WHERE id IN (${inList})`)\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`media:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[rel.fk_column] = fkVal ? (cache.get(`media:${fkVal}`) ?? null) : null\n }\n }\n}\n\n// Populates paragraph/junction fields with bare ID arrays when they are not\n// explicitly included — they have no column on the owning table, so without this\n// they'd be silently absent. Reference fields already carry their bare ID as a\n// real column from `SELECT *`, so they need no extra query.\nexport async function resolveRelationBareIds(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef\n): Promise<void> {\n if (rows.length === 0) return\n\n if (rel.type === 'paragraph') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT id, parent_id FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC, id ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of rows) {\n row[fieldName] = oneOrMany(rel.cardinality, (byParent[row['id'] as string] ?? []).map((r) => r['id']))\n }\n } else if (rel.type === 'junction') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT ${sql.raw(quoteIdent(rel.left_column))} AS left_id, ${sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`\n )\n const byLeft = groupBy(result.rows as Record<string, unknown>[], 'left_id')\n for (const row of rows) {\n row[fieldName] = firstOfEach(byLeft[row['id'] as string] ?? [], (r) => r['right_id'] as string).map((r) => r['right_id'])\n }\n }\n}\n","import { sql } from 'drizzle-orm'\nimport { relationCardinality } from '@bobbykim/manguito-cms-core'\nimport type { ParsedField, SchemaRegistry } from '@bobbykim/manguito-cms-core'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\n\n// Checks on relation values in an admin write, all run before any write.\n// checkRelationInput (shape), checkRequiredInParagraphItems and\n// findMissingReferences (one read per target table) each refuse an input that\n// would otherwise make Postgres fail part-way through the write (#54).\n//\n// Shape: Every relation field PRESENT in the body must match its cardinality:\n// 'one' paragraph → a plain object, or null 'one' reference → a UUID string, or null\n// 'many' paragraph → an array of plain objects 'many' reference → an array of UUID strings\n// Paragraph items are checked recursively; `path` prefixes nested names, so an\n// error names e.g. \"cards[0].card_link\". Absent fields are not checked: an\n// update leaves them untouched. Reference ids must be UUIDs: Postgres refuses\n// anything else (22P02) only after earlier statements of the write have run.\nexport type RelationInputError = { field: string; message: string }\n\n// The canonical 8-4-4-4-12 hex form, which Postgres's uuid type accepts.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nfunction isUuid(v: unknown): v is string {\n return typeof v === 'string' && UUID_RE.test(v)\n}\n\nexport function isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v)\n}\n\nexport function checkRelationInput(\n fields: ParsedField[],\n body: Record<string, unknown>,\n registry: SchemaRegistry,\n path = '',\n): RelationInputError[] {\n const errors: RelationInputError[] = []\n for (const field of fields) {\n if (!Object.hasOwn(body, field.name)) continue\n const cardinality = relationCardinality(field)\n if (cardinality === null) continue\n const value = body[field.name]\n const name = `${path}${field.name}`\n\n if (field.field_type === 'reference') {\n const ok =\n cardinality === 'one'\n ? value === null || isUuid(value)\n : Array.isArray(value) && value.every(isUuid)\n if (!ok) {\n errors.push({\n field: name,\n message:\n cardinality === 'one'\n ? `${name} holds one item: send a UUID string or null.`\n : `${name} holds a list: send an array of UUID strings.`,\n })\n }\n continue\n }\n\n const items =\n cardinality === 'one'\n ? value === null\n ? []\n : isPlainObject(value)\n ? [value]\n : undefined\n : Array.isArray(value) && value.every(isPlainObject)\n ? value\n : undefined\n if (items === undefined) {\n errors.push({\n field: name,\n message:\n cardinality === 'one'\n ? `${name} holds one item: send an object or null.`\n : `${name} holds a list: send an array of objects.`,\n })\n continue\n }\n\n const pType = paragraphTypeOf(field, registry)\n if (!pType) continue\n items.forEach((item, i) => {\n const prefix = cardinality === 'one' ? `${name}.` : `${name}[${i}].`\n errors.push(...checkRelationInput(pType.fields, item, registry, prefix))\n })\n }\n return errors\n}\n\n// The paragraph type a paragraph field embeds, or undefined for any other\n// field or an unknown ref.\nexport function paragraphTypeOf(field: ParsedField, registry: SchemaRegistry) {\n const comp = field.ui_component\n return comp.component === 'paragraph-embed' ? registry.paragraph_types[comp.ref] : undefined\n}\n\n// The paragraph items present in a body, each with the paragraph type's fields\n// and the path prefix its own fields are named under (\"cards[1].\" or\n// \"hero.\"). Assumes checkRelationInput passed, so values have the right shape.\nexport function paragraphItemsIn(\n fields: ParsedField[],\n body: Record<string, unknown>,\n registry: SchemaRegistry,\n path: string,\n): Array<{ item: Record<string, unknown>; fields: ParsedField[]; prefix: string }> {\n const out: Array<{ item: Record<string, unknown>; fields: ParsedField[]; prefix: string }> = []\n for (const field of fields) {\n if (field.field_type !== 'paragraph' || !Object.hasOwn(body, field.name)) continue\n const pType = paragraphTypeOf(field, registry)\n if (!pType) continue\n const value = body[field.name]\n const name = `${path}${field.name}`\n if (relationCardinality(field) === 'one') {\n if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` })\n } else if (Array.isArray(value)) {\n value.forEach((item, i) => {\n if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` })\n })\n }\n }\n return out\n}\n\n// What \"required\" refuses: no value, a blank string, or an empty list. Shared by\n// the top-level required check in the admin routes and the paragraph-item check\n// below, so the two levels cannot drift apart.\nexport function isMissing(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'string' && value.trim() === '') return true\n if (Array.isArray(value) && value.length === 0) return true\n return false\n}\n\n// Required fields inside paragraph items. A required field that has its own\n// column is NOT NULL in the paragraph table, so an item missing it makes\n// Postgres refuse the insert, after persistParagraphField has already deleted\n// the field's stored rows. Checked here, before any write, at every nesting\n// level. Column-less fields (a nested paragraph, a many-to-many list) cannot\n// make the insert fail and are not checked.\nexport function checkRequiredInParagraphItems(\n fields: ParsedField[],\n body: Record<string, unknown>,\n registry: SchemaRegistry,\n path = '',\n): RelationInputError[] {\n const errors: RelationInputError[] = []\n for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {\n for (const f of itemFields) {\n if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue\n if (isMissing(item[f.name])) {\n errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` })\n }\n }\n errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix))\n }\n return errors\n}\n\nfunction quoteIdent(name: string): string {\n if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`)\n return `\"${name}\"`\n}\n\ntype ReferenceUse = { table: string; id: string; field: string }\n\n// Every foreign-key id in the body, with the table it must exist in and the\n// path that names it: single references (\"owner\"), list entries (\"tags[1]\"),\n// media fields (\"hero_image\"), and all of these inside paragraph items\n// (\"cards[0].card_tag\", \"cards[0].card_image\").\nfunction collectReferences(\n fields: ParsedField[],\n body: Record<string, unknown>,\n registry: SchemaRegistry,\n path: string,\n out: ReferenceUse[],\n): void {\n for (const f of fields) {\n if (!Object.hasOwn(body, f.name) || !f.db_column) continue\n const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table\n if (!table) continue\n const value = body[f.name]\n const name = `${path}${f.name}`\n if (typeof value === 'string') {\n out.push({ table, id: value, field: name })\n } else if (Array.isArray(value)) {\n value.forEach((v, i) => {\n if (typeof v === 'string') out.push({ table, id: v, field: `${name}[${i}]` })\n })\n }\n }\n for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {\n collectReferences(itemFields, item, registry, prefix, out)\n }\n}\n\n// Foreign-key ids (references and media) that point at no row. A missing target\n// makes Postgres refuse the insert on its foreign key (23503) part-way through a\n// write that has already deleted the field's old links or paragraph rows. An id\n// that is not a UUID counts as missing without a query: Postgres would refuse it\n// as a uuid (22P02) inside the lookup itself. One query per target table, run in\n// parallel. Unpublished targets exist and pass.\nexport async function findMissingReferences(\n db: DrizzlePostgresInstance,\n fields: ParsedField[],\n body: Record<string, unknown>,\n registry: SchemaRegistry,\n): Promise<RelationInputError[]> {\n const uses: ReferenceUse[] = []\n collectReferences(fields, body, registry, '', uses)\n if (uses.length === 0) return []\n\n const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {\n const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))]\n if (ids.length === 0) return [table, new Set<string>()] as const\n const result = await db.execute(\n sql`SELECT id FROM ${sql.raw(quoteIdent(table))} WHERE id IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`\n )\n return [table, new Set((result.rows as Array<{ id: string }>).map((r) => String(r.id).toLowerCase()))] as const\n })\n const found = new Map(await Promise.all(lookups))\n\n return uses\n .filter((u) => !found.get(u.table)!.has(u.id.toLowerCase()))\n .map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }))\n}\n","import { EnvelopArmor } from '@escape.tech/graphql-armor'\nimport { NoSchemaIntrospectionCustomRule } from 'graphql'\n\n// Depth + cost + alias + directive limits via GraphQL Armor. Returns { plugins } to\n// spread into Yoga's `plugins` array.\n//\n// Note: the installed `@escape.tech/graphql-armor` (3.2.0) exports `EnvelopArmor`, a\n// class whose `.protect()` returns `{ plugins }` — the standalone `EnvelopArmorPlugin`\n// export is a function that returns a single plugin, not a class with `.protect()`.\nexport function buildArmorPlugin(options: { maxDepth: number; maxComplexity: number }): {\n plugins: unknown[]\n} {\n const armor = new EnvelopArmor({\n maxDepth: { n: options.maxDepth },\n costLimit: { maxCost: options.maxComplexity },\n maxAliases: { n: 15 },\n maxDirectives: { n: 50 },\n blockFieldSuggestion: { enabled: true },\n // maxTokens is intentionally left at Armor's secure default (n=1000) to bound\n // GraphQL document token count at parse time, a layer the depth/cost/alias/directive\n // limits do not cover.\n })\n return armor.protect()\n}\n\n// Disables introspection (and the __schema/__type meta-fields) in production by\n// adding graphql's built-in validation rule when `enabled` is false.\nexport function introspectionPlugin(enabled: boolean): {\n onValidate(payload: { addValidationRule: (rule: unknown) => void }): void\n} {\n return {\n onValidate({ addValidationRule }) {\n if (!enabled) addValidationRule(NoSchemaIntrospectionCustomRule)\n },\n }\n}\n"],"mappings":"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","names":["import_graphql","import_graphql","import_graphql","import_graphql","import_drizzle_orm","import_manguito_cms_core","import_manguito_cms_core","DataLoader","import_graphql"]}
|