@async/db 0.2.0
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/CHANGELOG.md +167 -0
- package/README.md +431 -0
- package/SPEC.md +1429 -0
- package/db.config.example.mjs +128 -0
- package/dist/cli/args.d.ts +8 -0
- package/dist/cli/args.js +16 -0
- package/dist/cli/commands/create.d.ts +3 -0
- package/dist/cli/commands/create.js +13 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +31 -0
- package/dist/cli/commands/generate.d.ts +6 -0
- package/dist/cli/commands/generate.js +24 -0
- package/dist/cli/commands/operations.d.ts +12 -0
- package/dist/cli/commands/operations.js +61 -0
- package/dist/cli/commands/schema.d.ts +11 -0
- package/dist/cli/commands/schema.js +1086 -0
- package/dist/cli/commands/serve.d.ts +9 -0
- package/dist/cli/commands/serve.js +18 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.js +11 -0
- package/dist/cli/commands/types.d.ts +7 -0
- package/dist/cli/commands/types.js +37 -0
- package/dist/cli/commands/viewer.d.ts +6 -0
- package/dist/cli/commands/viewer.js +29 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +108 -0
- package/dist/cli/output.d.ts +25 -0
- package/dist/cli/output.js +149 -0
- package/dist/cli/schema-prompt.d.ts +20 -0
- package/dist/cli/schema-prompt.js +66 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/client-cache.d.ts +105 -0
- package/dist/client-cache.js +916 -0
- package/dist/client.d.ts +64 -0
- package/dist/client.js +405 -0
- package/dist/config-public.d.ts +1 -0
- package/dist/config-public.js +1 -0
- package/dist/config.d.ts +54 -0
- package/dist/config.js +2 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1 -0
- package/dist/db.d.ts +3 -0
- package/dist/db.js +3 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +1 -0
- package/dist/features/config/defaults.d.ts +98 -0
- package/dist/features/config/defaults.js +95 -0
- package/dist/features/config/load.d.ts +11 -0
- package/dist/features/config/load.js +265 -0
- package/dist/features/config/public.d.ts +17 -0
- package/dist/features/config/public.js +75 -0
- package/dist/features/doctor/duplicate-ids.d.ts +18 -0
- package/dist/features/doctor/duplicate-ids.js +79 -0
- package/dist/features/doctor/field-consistency.d.ts +17 -0
- package/dist/features/doctor/field-consistency.js +48 -0
- package/dist/features/doctor/index.d.ts +39 -0
- package/dist/features/doctor/index.js +177 -0
- package/dist/features/doctor/relations.d.ts +22 -0
- package/dist/features/doctor/relations.js +90 -0
- package/dist/features/doctor/schema-guidance.d.ts +35 -0
- package/dist/features/doctor/schema-guidance.js +184 -0
- package/dist/features/generate/registry.d.ts +14 -0
- package/dist/features/generate/registry.js +37 -0
- package/dist/features/http/registry.d.ts +46 -0
- package/dist/features/http/registry.js +86 -0
- package/dist/features/operations/index.d.ts +49 -0
- package/dist/features/operations/index.js +199 -0
- package/dist/features/operations/maps.d.ts +1 -0
- package/dist/features/operations/maps.js +10 -0
- package/dist/features/operations/readiness.d.ts +30 -0
- package/dist/features/operations/readiness.js +228 -0
- package/dist/features/operations/runtime.d.ts +57 -0
- package/dist/features/operations/runtime.js +288 -0
- package/dist/features/runtime/collection.d.ts +51 -0
- package/dist/features/runtime/collection.js +198 -0
- package/dist/features/runtime/db.d.ts +152 -0
- package/dist/features/runtime/db.js +824 -0
- package/dist/features/runtime/document.d.ts +43 -0
- package/dist/features/runtime/document.js +111 -0
- package/dist/features/runtime/fanout.d.ts +24 -0
- package/dist/features/runtime/fanout.js +77 -0
- package/dist/features/runtime/json-pointer.d.ts +5 -0
- package/dist/features/runtime/json-pointer.js +49 -0
- package/dist/features/runtime/scope-state.d.ts +44 -0
- package/dist/features/runtime/scope-state.js +185 -0
- package/dist/features/runtime/state.d.ts +1 -0
- package/dist/features/runtime/state.js +1 -0
- package/dist/features/schema/api.d.ts +107 -0
- package/dist/features/schema/api.js +460 -0
- package/dist/features/schema/builders.d.ts +86 -0
- package/dist/features/schema/builders.js +110 -0
- package/dist/features/schema/fields.d.ts +38 -0
- package/dist/features/schema/fields.js +296 -0
- package/dist/features/schema/generated.d.ts +29 -0
- package/dist/features/schema/generated.js +32 -0
- package/dist/features/schema/locator.d.ts +16 -0
- package/dist/features/schema/locator.js +135 -0
- package/dist/features/schema/manifest.d.ts +91 -0
- package/dist/features/schema/manifest.js +384 -0
- package/dist/features/schema/metadata.d.ts +30 -0
- package/dist/features/schema/metadata.js +75 -0
- package/dist/features/schema/project.d.ts +46 -0
- package/dist/features/schema/project.js +442 -0
- package/dist/features/schema/relations.d.ts +38 -0
- package/dist/features/schema/relations.js +109 -0
- package/dist/features/schema/resolvers.d.ts +36 -0
- package/dist/features/schema/resolvers.js +111 -0
- package/dist/features/schema/resource.d.ts +75 -0
- package/dist/features/schema/resource.js +253 -0
- package/dist/features/schema/source-definitions.d.ts +21 -0
- package/dist/features/schema/source-definitions.js +29 -0
- package/dist/features/schema/sources.d.ts +83 -0
- package/dist/features/schema/sources.js +689 -0
- package/dist/features/schema/standard-schema.d.ts +57 -0
- package/dist/features/schema/standard-schema.js +232 -0
- package/dist/features/schema/validation.d.ts +69 -0
- package/dist/features/schema/validation.js +434 -0
- package/dist/features/storage/events.d.ts +12 -0
- package/dist/features/storage/events.js +30 -0
- package/dist/features/storage/json.d.ts +112 -0
- package/dist/features/storage/json.js +239 -0
- package/dist/features/storage/memory.d.ts +30 -0
- package/dist/features/storage/memory.js +44 -0
- package/dist/features/storage/resource-json.d.ts +31 -0
- package/dist/features/storage/resource-json.js +76 -0
- package/dist/features/storage/runtime.d.ts +37 -0
- package/dist/features/storage/runtime.js +184 -0
- package/dist/features/storage/source-metadata.d.ts +20 -0
- package/dist/features/storage/source-metadata.js +25 -0
- package/dist/features/storage/source.d.ts +37 -0
- package/dist/features/storage/source.js +60 -0
- package/dist/features/storage/static.d.ts +29 -0
- package/dist/features/storage/static.js +42 -0
- package/dist/features/sync/defaults.d.ts +21 -0
- package/dist/features/sync/defaults.js +21 -0
- package/dist/features/sync/index.d.ts +35 -0
- package/dist/features/sync/index.js +85 -0
- package/dist/features/sync/mirror-state.d.ts +14 -0
- package/dist/features/sync/mirror-state.js +4 -0
- package/dist/features/sync/runtime-dirs.d.ts +5 -0
- package/dist/features/sync/runtime-dirs.js +9 -0
- package/dist/features/sync/source-writes.d.ts +15 -0
- package/dist/features/sync/source-writes.js +27 -0
- package/dist/features/sync/synthetic-seed.d.ts +26 -0
- package/dist/features/sync/synthetic-seed.js +83 -0
- package/dist/features/viewer/manifest.d.ts +148 -0
- package/dist/features/viewer/manifest.js +165 -0
- package/dist/fs-utils.d.ts +1 -0
- package/dist/fs-utils.js +1 -0
- package/dist/generate/hono/app.d.ts +6 -0
- package/dist/generate/hono/app.js +51 -0
- package/dist/generate/hono/graphql.d.ts +7 -0
- package/dist/generate/hono/graphql.js +53 -0
- package/dist/generate/hono/index.d.ts +55 -0
- package/dist/generate/hono/index.js +140 -0
- package/dist/generate/hono/package.d.ts +6 -0
- package/dist/generate/hono/package.js +44 -0
- package/dist/generate/hono/readme.d.ts +13 -0
- package/dist/generate/hono/readme.js +28 -0
- package/dist/generate/hono/repository.d.ts +1 -0
- package/dist/generate/hono/repository.js +27 -0
- package/dist/generate/hono/rest.d.ts +1 -0
- package/dist/generate/hono/rest.js +38 -0
- package/dist/generate/hono/schema.d.ts +13 -0
- package/dist/generate/hono/schema.js +18 -0
- package/dist/generate/hono/sqlite.d.ts +20 -0
- package/dist/generate/hono/sqlite.js +266 -0
- package/dist/generate/hono/validators.d.ts +1 -0
- package/dist/generate/hono/validators.js +141 -0
- package/dist/generate/hono.d.ts +1 -0
- package/dist/generate/hono.js +1 -0
- package/dist/graphql/execute.d.ts +14 -0
- package/dist/graphql/execute.js +719 -0
- package/dist/graphql/http.d.ts +15 -0
- package/dist/graphql/http.js +29 -0
- package/dist/graphql/index.d.ts +3 -0
- package/dist/graphql/index.js +3 -0
- package/dist/graphql/parser.d.ts +54 -0
- package/dist/graphql/parser.js +433 -0
- package/dist/hono.d.ts +77 -0
- package/dist/hono.js +1 -0
- package/dist/index.d.ts +1065 -0
- package/dist/index.js +14 -0
- package/dist/integrations/hono.d.ts +136 -0
- package/dist/integrations/hono.js +508 -0
- package/dist/integrations/kv.d.ts +69 -0
- package/dist/integrations/kv.js +69 -0
- package/dist/integrations/postgres.d.ts +52 -0
- package/dist/integrations/postgres.js +113 -0
- package/dist/integrations/sqlite.d.ts +112 -0
- package/dist/integrations/sqlite.js +489 -0
- package/dist/integrations/vite.d.ts +45 -0
- package/dist/integrations/vite.js +111 -0
- package/dist/json.d.ts +48 -0
- package/dist/json.js +1 -0
- package/dist/jsonc.d.ts +1 -0
- package/dist/jsonc.js +1 -0
- package/dist/kv.d.ts +24 -0
- package/dist/kv.js +1 -0
- package/dist/mock.d.ts +1 -0
- package/dist/mock.js +1 -0
- package/dist/names.d.ts +1 -0
- package/dist/names.js +1 -0
- package/dist/operations.d.ts +3 -0
- package/dist/operations.js +3 -0
- package/dist/postgres.d.ts +24 -0
- package/dist/postgres.js +1 -0
- package/dist/redis.d.ts +14 -0
- package/dist/redis.js +1 -0
- package/dist/rest/formats.d.ts +80 -0
- package/dist/rest/formats.js +318 -0
- package/dist/rest/handler.d.ts +111 -0
- package/dist/rest/handler.js +833 -0
- package/dist/rest/shape.d.ts +33 -0
- package/dist/rest/shape.js +218 -0
- package/dist/schema-builders.d.ts +1 -0
- package/dist/schema-builders.js +1 -0
- package/dist/schema-manifest.d.ts +1 -0
- package/dist/schema-manifest.js +1 -0
- package/dist/schema.d.ts +193 -0
- package/dist/schema.js +6 -0
- package/dist/server.d.ts +116 -0
- package/dist/server.js +601 -0
- package/dist/shared/csv.d.ts +8 -0
- package/dist/shared/csv.js +149 -0
- package/dist/shared/errors.d.ts +40 -0
- package/dist/shared/errors.js +55 -0
- package/dist/shared/fs-utils.d.ts +4 -0
- package/dist/shared/fs-utils.js +30 -0
- package/dist/shared/jsonc.d.ts +2 -0
- package/dist/shared/jsonc.js +99 -0
- package/dist/shared/mock.d.ts +40 -0
- package/dist/shared/mock.js +83 -0
- package/dist/shared/names.d.ts +28 -0
- package/dist/shared/names.js +127 -0
- package/dist/shared/operations.d.ts +32 -0
- package/dist/shared/operations.js +302 -0
- package/dist/sqlite.d.ts +24 -0
- package/dist/sqlite.js +1 -0
- package/dist/state.d.ts +1 -0
- package/dist/state.js +1 -0
- package/dist/sync.d.ts +1 -0
- package/dist/sync.js +1 -0
- package/dist/tracing.d.ts +95 -0
- package/dist/tracing.js +260 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +285 -0
- package/dist/viewer-manifest.d.ts +1 -0
- package/dist/viewer-manifest.js +1 -0
- package/dist/vite.d.ts +59 -0
- package/dist/vite.js +1 -0
- package/dist/web/json-viewer.d.ts +5 -0
- package/dist/web/json-viewer.js +176 -0
- package/dist/web/viewer.d.ts +12 -0
- package/dist/web/viewer.js +1015 -0
- package/docs/README.md +42 -0
- package/docs/architecture.md +112 -0
- package/docs/ci-and-release.md +177 -0
- package/docs/cms-storage-patterns.md +108 -0
- package/docs/concepts.md +141 -0
- package/docs/configuration.md +552 -0
- package/docs/fixtures-and-schemas.md +527 -0
- package/docs/fork-branch-workflows.md +108 -0
- package/docs/generated-files.md +174 -0
- package/docs/getting-started.md +165 -0
- package/docs/integrations.md +206 -0
- package/docs/json-production.md +120 -0
- package/docs/package-api.md +418 -0
- package/docs/prototype-to-production.md +378 -0
- package/docs/server-and-viewer.md +466 -0
- package/docs/store-graduation.md +120 -0
- package/docs/typescript-schema-sources.md +79 -0
- package/examples/advanced/README.md +55 -0
- package/examples/advanced/db/projects.schema.jsonc +44 -0
- package/examples/advanced/db/settings.jsonc +9 -0
- package/examples/advanced/db/users.json +23 -0
- package/examples/advanced/db/users.schema.mjs +31 -0
- package/examples/advanced/db.config.mjs +18 -0
- package/examples/advanced/example.json +5 -0
- package/examples/advanced/src/generated/db.types.d.ts +64 -0
- package/examples/basic/README.md +95 -0
- package/examples/basic/db/operations/get-user.jsonc +8 -0
- package/examples/basic/db/settings.json +7 -0
- package/examples/basic/db/users.schema.jsonc +36 -0
- package/examples/basic/db.config.mjs +68 -0
- package/examples/basic/example.json +5 -0
- package/examples/basic/src/generated/db.types.d.ts +39 -0
- package/examples/cms-json-publish/README.md +21 -0
- package/examples/cms-json-publish/db/navigation.json +7 -0
- package/examples/cms-json-publish/db/pages.json +18 -0
- package/examples/cms-json-publish/example.json +5 -0
- package/examples/cms-json-publish/src/cms.mjs +104 -0
- package/examples/computed-fields/README.md +93 -0
- package/examples/computed-fields/db/orders.schema.mjs +62 -0
- package/examples/computed-fields/db/posts.schema.mjs +59 -0
- package/examples/computed-fields/db/products.schema.mjs +39 -0
- package/examples/computed-fields/db/users.schema.mjs +43 -0
- package/examples/computed-fields/db.config.mjs +15 -0
- package/examples/computed-fields/example.json +5 -0
- package/examples/computed-fields/src/generated/db.types.d.ts +81 -0
- package/examples/content-collections/README.md +91 -0
- package/examples/content-collections/db/authors.json +12 -0
- package/examples/content-collections/db/authors.schema.mjs +20 -0
- package/examples/content-collections/db/blog/draft-roadmap.mdx +12 -0
- package/examples/content-collections/db/blog/index.schema.mjs +61 -0
- package/examples/content-collections/db/blog/launch-notes.mdx +15 -0
- package/examples/content-collections/db/docs/index.schema.mjs +32 -0
- package/examples/content-collections/db/docs/intro.mdx +11 -0
- package/examples/content-collections/db/docs/schema-workflow.mdx +10 -0
- package/examples/content-collections/db/site.schema.jsonc +21 -0
- package/examples/content-collections/db.config.mjs +26 -0
- package/examples/content-collections/example.json +5 -0
- package/examples/content-collections/src/content-preview.mjs +66 -0
- package/examples/content-collections/src/generated/db.types.d.ts +81 -0
- package/examples/csv/README.md +52 -0
- package/examples/csv/db/customers.csv +4 -0
- package/examples/csv/db.config.mjs +13 -0
- package/examples/csv/example.json +5 -0
- package/examples/data-first/README.md +54 -0
- package/examples/data-first/db/posts.json +16 -0
- package/examples/data-first/db/settings.json +8 -0
- package/examples/data-first/db/users.json +14 -0
- package/examples/data-first/db.config.mjs +13 -0
- package/examples/data-first/example.json +5 -0
- package/examples/diagnostics/README.md +55 -0
- package/examples/diagnostics/db/projects.schema.jsonc +27 -0
- package/examples/diagnostics/db/users.json +9 -0
- package/examples/diagnostics/db/users.schema.jsonc +23 -0
- package/examples/diagnostics/db.config.mjs +16 -0
- package/examples/diagnostics/example.json +5 -0
- package/examples/free-plan-upgrade/README.md +22 -0
- package/examples/free-plan-upgrade/db/appSettings.json +4 -0
- package/examples/free-plan-upgrade/db/projects.json +7 -0
- package/examples/free-plan-upgrade/example.json +5 -0
- package/examples/free-plan-upgrade/src/upgrade-tenant-to-paid.mjs +105 -0
- package/examples/hono-auth/README.md +74 -0
- package/examples/hono-auth/db/pages.schema.jsonc +44 -0
- package/examples/hono-auth/db/users.schema.jsonc +42 -0
- package/examples/hono-auth/db.config.mjs +17 -0
- package/examples/hono-auth/example.json +5 -0
- package/examples/hono-auth/package.json +14 -0
- package/examples/hono-auth/src/app.mjs +79 -0
- package/examples/hono-auth/src/server.mjs +13 -0
- package/examples/production-json/README.md +102 -0
- package/examples/production-json/db/appSettings.schema.jsonc +41 -0
- package/examples/production-json/db/featureFlags.schema.jsonc +84 -0
- package/examples/production-json/db/operations/get-control-plane.jsonc +6 -0
- package/examples/production-json/db/operations/get-feature-flag.jsonc +9 -0
- package/examples/production-json/db/operations/list-feature-flags.jsonc +8 -0
- package/examples/production-json/db/operations/read-public-settings.jsonc +8 -0
- package/examples/production-json/db.config.mjs +33 -0
- package/examples/production-json/example.json +5 -0
- package/examples/production-json/src/client-demo.mjs +28 -0
- package/examples/production-json/src/generated/db.types.d.ts +60 -0
- package/examples/relations/README.md +56 -0
- package/examples/relations/db/posts.schema.jsonc +46 -0
- package/examples/relations/db/users.schema.jsonc +34 -0
- package/examples/relations/db.config.mjs +13 -0
- package/examples/relations/example.json +5 -0
- package/examples/rest-client/README.md +54 -0
- package/examples/rest-client/db/settings.json +5 -0
- package/examples/rest-client/db/users.schema.jsonc +42 -0
- package/examples/rest-client/db.config.mjs +13 -0
- package/examples/rest-client/example.json +5 -0
- package/examples/rest-client/src/client-demo.mjs +24 -0
- package/examples/schema-first/README.md +55 -0
- package/examples/schema-first/db/auditEvents.schema.jsonc +24 -0
- package/examples/schema-first/db/settings.schema.jsonc +29 -0
- package/examples/schema-first/db/users.schema.jsonc +36 -0
- package/examples/schema-first/db.config.mjs +15 -0
- package/examples/schema-first/example.json +5 -0
- package/examples/schema-first/src/generated/db.types.d.ts +47 -0
- package/examples/schema-manifest/README.md +50 -0
- package/examples/schema-manifest/db/projects.schema.jsonc +48 -0
- package/examples/schema-manifest/db/users.schema.jsonc +35 -0
- package/examples/schema-manifest/db.config.mjs +41 -0
- package/examples/schema-manifest/example.json +5 -0
- package/examples/schema-manifest/src/generated/db.schema.json +130 -0
- package/examples/schema-manifest/src/generated/db.types.d.ts +50 -0
- package/examples/schema-ui/README.md +103 -0
- package/examples/schema-ui/db/pages.schema.jsonc +53 -0
- package/examples/schema-ui/db/users.schema.jsonc +30 -0
- package/examples/schema-ui/db.config.mjs +55 -0
- package/examples/schema-ui/example.json +5 -0
- package/examples/schema-ui/src/cms-ssr.mjs +276 -0
- package/examples/schema-ui/src/generated/db.schema.json +133 -0
- package/examples/schema-ui/src/generated/db.types.d.ts +46 -0
- package/examples/schema-ui/src/render-admin.mjs +175 -0
- package/examples/schema-ui/src/schema-ui-ssr-handler.mjs +149 -0
- package/examples/schema-ui/src/start-schema-ui-server.mjs +140 -0
- package/examples/standard-schema/README.md +55 -0
- package/examples/standard-schema/db/settings.schema.mjs +22 -0
- package/examples/standard-schema/db/users.schema.mjs +72 -0
- package/examples/standard-schema/example.json +5 -0
- package/package.json +108 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseJsonc } from '../../jsonc.js';
|
|
5
|
+
import { buildResource } from './resource.js';
|
|
6
|
+
import { duplicateResourceDiagnostics, listSourceFiles, readRootSchemaFile, readSourceFile, trackResourceSource } from './sources.js';
|
|
7
|
+
import { makeGeneratedSchema } from './generated.js';
|
|
8
|
+
import { resourceAliasCollisionGroups } from '../../names.js';
|
|
9
|
+
import { validateProjectRelations } from './relations.js';
|
|
10
|
+
import { validateResourceSeed } from './validation.js';
|
|
11
|
+
import { normalizeSchemaLoadMode } from './locator.js';
|
|
12
|
+
import { normalizeFilesSource } from './source-definitions.js';
|
|
13
|
+
export async function loadProjectSchema(config, options = {}) {
|
|
14
|
+
const loadMode = normalizeSchemaLoadMode(options.load ?? config.schemaLoadMode ?? 'data');
|
|
15
|
+
const rootSchema = await readRootSchemaFile(config);
|
|
16
|
+
const files = await sourceFilesForLoadMode(config, rootSchema, loadMode);
|
|
17
|
+
const dataFiles = new Map();
|
|
18
|
+
const schemaFiles = new Map();
|
|
19
|
+
const resourceSources = new Map();
|
|
20
|
+
const diagnostics = [...rootSchema.diagnostics];
|
|
21
|
+
for (const source of rootSchema.sources) {
|
|
22
|
+
trackResourceSource(resourceSources, source.name, source.file, source.kind);
|
|
23
|
+
schemaFiles.set(source.name, source);
|
|
24
|
+
}
|
|
25
|
+
for (const filename of files) {
|
|
26
|
+
if (rootSchema.found && isSchemaSourceFilename(filename)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const result = await readSourceFile(config, filename);
|
|
30
|
+
diagnostics.push(...result.diagnostics);
|
|
31
|
+
for (const source of result.sources) {
|
|
32
|
+
trackResourceSource(resourceSources, source.name, source.file, source.kind);
|
|
33
|
+
if (source.kind === 'schema') {
|
|
34
|
+
schemaFiles.set(source.name, source);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
dataFiles.set(source.name, source);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const source of schemaFiles.values()) {
|
|
42
|
+
if (config.schema?.source !== 'schema' && !dataFiles.has(source.name) && source.schema?.source) {
|
|
43
|
+
if (loadMode === 'schema') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const contentSource = await contentDataSourceForSchema(config, source);
|
|
47
|
+
diagnostics.push(...contentSource.diagnostics);
|
|
48
|
+
if (contentSource.source) {
|
|
49
|
+
dataFiles.set(source.name, contentSource.source);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const resourceNames = [...new Set([...dataFiles.keys(), ...schemaFiles.keys()])].sort();
|
|
54
|
+
const resources = [];
|
|
55
|
+
diagnostics.push(...duplicateResourceDiagnostics(resourceSources));
|
|
56
|
+
for (const name of resourceNames) {
|
|
57
|
+
const dataSource = dataFiles.get(name);
|
|
58
|
+
const schemaSource = schemaFiles.get(name);
|
|
59
|
+
const rawData = dataSource?.data;
|
|
60
|
+
const rawSchema = schemaSource?.schema;
|
|
61
|
+
if (rawData === undefined && rawSchema === undefined) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (rawData !== undefined && rawSchema && Object.prototype.hasOwnProperty.call(rawSchema, 'seed')) {
|
|
65
|
+
diagnostics.push(mixedModeSchemaSeedDiagnostic(name, dataSource, schemaSource));
|
|
66
|
+
}
|
|
67
|
+
if (schemaSource && isFolderSchemaMarker(schemaSource) && !rawSchema?.source) {
|
|
68
|
+
diagnostics.push(folderSourceRequiredDiagnostic(name, schemaSource));
|
|
69
|
+
}
|
|
70
|
+
if (rawSchema?.store) {
|
|
71
|
+
diagnostics.push(schemaStoreIgnoredDiagnostic(name, schemaSource));
|
|
72
|
+
}
|
|
73
|
+
if (rawSchema?.parser) {
|
|
74
|
+
diagnostics.push(schemaParserDeprecatedDiagnostic(name, schemaSource));
|
|
75
|
+
}
|
|
76
|
+
const resource = buildResource({
|
|
77
|
+
name,
|
|
78
|
+
dataPath: dataSource?.sourceFile,
|
|
79
|
+
dataFormat: dataSource?.format,
|
|
80
|
+
dataHash: dataSource?.hash,
|
|
81
|
+
schemaPath: schemaSource?.sourceFile,
|
|
82
|
+
schemaSource: schemaSource?.format,
|
|
83
|
+
rawData,
|
|
84
|
+
rawSchema,
|
|
85
|
+
config,
|
|
86
|
+
includeSeed: loadMode !== 'schema',
|
|
87
|
+
});
|
|
88
|
+
if (loadMode !== 'schema') {
|
|
89
|
+
diagnostics.push(...validateResourceSeed(resource, config));
|
|
90
|
+
}
|
|
91
|
+
diagnostics.push(...(resource.diagnostics ?? []));
|
|
92
|
+
resources.push(resource);
|
|
93
|
+
}
|
|
94
|
+
diagnostics.push(...validateProjectRelations(resources));
|
|
95
|
+
diagnostics.push(...resourceAliasCollisionDiagnostics(resources));
|
|
96
|
+
return {
|
|
97
|
+
resources,
|
|
98
|
+
diagnostics,
|
|
99
|
+
schema: makeGeneratedSchema(resources, diagnostics),
|
|
100
|
+
loadMode,
|
|
101
|
+
locator: config.schemaLocator ?? null,
|
|
102
|
+
rootSchema: {
|
|
103
|
+
found: rootSchema.found,
|
|
104
|
+
file: rootSchema.file,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function isSchemaSourceFilename(filename) {
|
|
109
|
+
return filename.endsWith('.schema.json') || filename.endsWith('.schema.jsonc') || filename.endsWith('.schema.mjs') || filename.endsWith('.schema.js');
|
|
110
|
+
}
|
|
111
|
+
async function sourceFilesForLoadMode(config, rootSchema, loadMode) {
|
|
112
|
+
const locator = config.schemaLocator;
|
|
113
|
+
if (locator?.mode === 'schema-file') {
|
|
114
|
+
return singleSchemaFileSources(config, locator, loadMode);
|
|
115
|
+
}
|
|
116
|
+
const files = await listSourceFiles(config);
|
|
117
|
+
if (loadMode === 'schema') {
|
|
118
|
+
return files.filter(isSchemaSourceFilename);
|
|
119
|
+
}
|
|
120
|
+
return files;
|
|
121
|
+
}
|
|
122
|
+
async function singleSchemaFileSources(config, locator, loadMode) {
|
|
123
|
+
const files = await listSourceFiles(config);
|
|
124
|
+
const schemaFile = path.relative(config.sourceDir ?? '', locator.file);
|
|
125
|
+
const normalizedSchemaFile = schemaFile.split(path.sep).join('/');
|
|
126
|
+
const available = new Set(files.map((file) => file.split(path.sep).join('/')));
|
|
127
|
+
const selected = available.has(normalizedSchemaFile) ? [normalizedSchemaFile] : [];
|
|
128
|
+
if (loadMode === 'schema') {
|
|
129
|
+
return selected;
|
|
130
|
+
}
|
|
131
|
+
for (const dataFile of siblingDataFilesForSchema(normalizedSchemaFile)) {
|
|
132
|
+
if (available.has(dataFile)) {
|
|
133
|
+
selected.push(dataFile);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return selected;
|
|
137
|
+
}
|
|
138
|
+
function siblingDataFilesForSchema(schemaFile) {
|
|
139
|
+
const base = schemaFile.replace(/\.schema\.(?:json|jsonc|mjs|js)$/i, '');
|
|
140
|
+
return [`${base}.json`, `${base}.jsonc`, `${base}.csv`];
|
|
141
|
+
}
|
|
142
|
+
function isFolderSchemaMarker(schemaSource) {
|
|
143
|
+
const file = schemaSource.file.split(path.sep).join('/');
|
|
144
|
+
return file.endsWith('/index.schema.mjs') || file.endsWith('/index.schema.js');
|
|
145
|
+
}
|
|
146
|
+
async function contentDataSourceForSchema(config, schemaSource) {
|
|
147
|
+
const source = normalizeFilesSource(schemaSource.schema?.source, { read: schemaSource.schema?.parser });
|
|
148
|
+
const baseDir = schemaSource.baseDir ?? path.dirname(schemaSource.sourceFile);
|
|
149
|
+
const files = [];
|
|
150
|
+
const diagnostics = [];
|
|
151
|
+
for (const pattern of source?.patterns ?? []) {
|
|
152
|
+
const matched = await filesMatchingGlob(baseDir, String(pattern));
|
|
153
|
+
files.push(...matched);
|
|
154
|
+
}
|
|
155
|
+
const uniqueFiles = [...new Set(files)].sort();
|
|
156
|
+
const records = [];
|
|
157
|
+
const hash = createHash('sha256');
|
|
158
|
+
for (const filePath of uniqueFiles) {
|
|
159
|
+
let text;
|
|
160
|
+
try {
|
|
161
|
+
text = await readFile(filePath, 'utf8');
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
diagnostics.push(contentLoadDiagnostic(config, schemaSource, filePath, error));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
hash.update(filePath);
|
|
168
|
+
hash.update('\0');
|
|
169
|
+
hash.update(text);
|
|
170
|
+
try {
|
|
171
|
+
records.push(parseContentRecord(schemaSource, filePath, text, source.read));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
diagnostics.push(contentLoadDiagnostic(config, schemaSource, filePath, error));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
source: {
|
|
179
|
+
kind: 'data',
|
|
180
|
+
name: schemaSource.name,
|
|
181
|
+
file: schemaSource.file,
|
|
182
|
+
sourceFile: schemaSource.sourceFile,
|
|
183
|
+
format: source?.read ?? 'frontmatter',
|
|
184
|
+
hash: hash.digest('hex'),
|
|
185
|
+
data: records,
|
|
186
|
+
baseDir,
|
|
187
|
+
},
|
|
188
|
+
diagnostics,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function filesMatchingGlob(baseDir, pattern) {
|
|
192
|
+
const normalizedPattern = normalizeSlash(pattern).replace(/^\.\//, '');
|
|
193
|
+
const firstGlob = firstGlobIndex(normalizedPattern);
|
|
194
|
+
const rootPart = firstGlob === -1
|
|
195
|
+
? path.dirname(normalizedPattern)
|
|
196
|
+
: normalizedPattern.slice(0, firstGlob).split('/').slice(0, -1).join('/');
|
|
197
|
+
const searchRoot = path.resolve(baseDir, rootPart || '.');
|
|
198
|
+
const allFiles = await listFilesRecursive(searchRoot);
|
|
199
|
+
const regexp = globRegExp(normalizedPattern);
|
|
200
|
+
return allFiles.filter((filePath) => {
|
|
201
|
+
const relative = normalizeSlash(path.relative(baseDir, filePath));
|
|
202
|
+
return regexp.test(relative) || regexp.test(`./${relative}`);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function listFilesRecursive(directory) {
|
|
206
|
+
let entries;
|
|
207
|
+
try {
|
|
208
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error.code === 'ENOENT') {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
const files = [];
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const fullPath = path.join(directory, entry.name);
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
files.push(...await listFilesRecursive(fullPath));
|
|
224
|
+
}
|
|
225
|
+
else if (entry.isFile()) {
|
|
226
|
+
files.push(fullPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return files;
|
|
230
|
+
}
|
|
231
|
+
function firstGlobIndex(pattern) {
|
|
232
|
+
const indexes = ['*', '?', '[', '{']
|
|
233
|
+
.map((token) => pattern.indexOf(token))
|
|
234
|
+
.filter((index) => index >= 0);
|
|
235
|
+
return indexes.length === 0 ? -1 : Math.min(...indexes);
|
|
236
|
+
}
|
|
237
|
+
function globRegExp(pattern) {
|
|
238
|
+
let source = '^';
|
|
239
|
+
const normalized = normalizeSlash(pattern);
|
|
240
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
241
|
+
const char = normalized[index];
|
|
242
|
+
const next = normalized[index + 1];
|
|
243
|
+
if (char === '*' && next === '*') {
|
|
244
|
+
if (normalized[index + 2] === '/') {
|
|
245
|
+
source += '(?:.*\\/)?';
|
|
246
|
+
index += 2;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
source += '.*';
|
|
250
|
+
index += 1;
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (char === '*') {
|
|
255
|
+
source += '[^/]*';
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (char === '?') {
|
|
259
|
+
source += '[^/]';
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
source += escapeRegExp(char);
|
|
263
|
+
}
|
|
264
|
+
source += '$';
|
|
265
|
+
return new RegExp(source);
|
|
266
|
+
}
|
|
267
|
+
function parseContentRecord(schemaSource, filePath, text, read = 'frontmatter') {
|
|
268
|
+
if (read === 'json') {
|
|
269
|
+
return JSON.parse(text);
|
|
270
|
+
}
|
|
271
|
+
if (read === 'jsonc') {
|
|
272
|
+
return parseJsonc(text, filePath);
|
|
273
|
+
}
|
|
274
|
+
if (read === 'text') {
|
|
275
|
+
return {
|
|
276
|
+
id: basenameId(filePath),
|
|
277
|
+
body: text,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return frontmatterRecord(filePath, text);
|
|
281
|
+
}
|
|
282
|
+
function frontmatterRecord(filePath, text) {
|
|
283
|
+
const { data, body } = parseFrontmatter(text);
|
|
284
|
+
return {
|
|
285
|
+
id: data.id ?? basenameId(filePath),
|
|
286
|
+
...data,
|
|
287
|
+
body: body.trim(),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function parseFrontmatter(text) {
|
|
291
|
+
if (!text.startsWith('---')) {
|
|
292
|
+
return {
|
|
293
|
+
data: {},
|
|
294
|
+
body: text,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const lines = text.split(/\r?\n/);
|
|
298
|
+
const closeIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
299
|
+
if (closeIndex === -1) {
|
|
300
|
+
return {
|
|
301
|
+
data: {},
|
|
302
|
+
body: text,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
data: parseFrontmatterData(lines.slice(1, closeIndex)),
|
|
307
|
+
body: lines.slice(closeIndex + 1).join('\n'),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function parseFrontmatterData(lines) {
|
|
311
|
+
const data = {};
|
|
312
|
+
for (const line of lines) {
|
|
313
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
314
|
+
if (!match) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
data[match[1]] = parseFrontmatterValue(match[2]);
|
|
318
|
+
}
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
function parseFrontmatterValue(value) {
|
|
322
|
+
const trimmed = value.trim();
|
|
323
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
324
|
+
return trimmed.slice(1, -1);
|
|
325
|
+
}
|
|
326
|
+
if (trimmed === 'true') {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (trimmed === 'false') {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (/^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmed)) {
|
|
333
|
+
return Number(trimmed);
|
|
334
|
+
}
|
|
335
|
+
return trimmed;
|
|
336
|
+
}
|
|
337
|
+
function basenameId(filePath) {
|
|
338
|
+
return path.basename(filePath).replace(/\.[^.]+$/, '');
|
|
339
|
+
}
|
|
340
|
+
function contentLoadDiagnostic(config, schemaSource, filePath, error) {
|
|
341
|
+
const relative = path.relative(config.cwd ?? '.', filePath);
|
|
342
|
+
const parserMessage = error instanceof Error ? error.message : String(error);
|
|
343
|
+
const errorCode = error?.code;
|
|
344
|
+
return {
|
|
345
|
+
code: 'CONTENT_SOURCE_LOAD_FAILED',
|
|
346
|
+
severity: 'error',
|
|
347
|
+
resource: schemaSource.name,
|
|
348
|
+
file: relative,
|
|
349
|
+
message: `Could not load content source ${relative}: ${parserMessage}`,
|
|
350
|
+
hint: 'Fix this content file and db will reload the rest of the project.',
|
|
351
|
+
details: {
|
|
352
|
+
resource: schemaSource.name,
|
|
353
|
+
path: relative,
|
|
354
|
+
parserMessage,
|
|
355
|
+
code: errorCode,
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function folderSourceRequiredDiagnostic(resource, schemaSource) {
|
|
360
|
+
return {
|
|
361
|
+
code: 'SCHEMA_UNBUNDLE_FOLDER_SOURCE_REQUIRED',
|
|
362
|
+
severity: 'error',
|
|
363
|
+
resource,
|
|
364
|
+
file: schemaSource.file,
|
|
365
|
+
message: `Folder collection marker ${schemaSource.file} must declare an explicit source glob.`,
|
|
366
|
+
hint: `Add source: files('./**/*.mdx', { read: 'frontmatter' }) or another explicit files() source to ${schemaSource.file}.`,
|
|
367
|
+
details: {
|
|
368
|
+
command: 'schema unbundle --all',
|
|
369
|
+
resource,
|
|
370
|
+
file: schemaSource.file,
|
|
371
|
+
marker: 'index.schema.mjs',
|
|
372
|
+
requiredProperty: 'source',
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function schemaStoreIgnoredDiagnostic(resource, schemaSource) {
|
|
377
|
+
return {
|
|
378
|
+
code: 'SCHEMA_STORE_IGNORED',
|
|
379
|
+
severity: 'warn',
|
|
380
|
+
resource,
|
|
381
|
+
file: schemaSource.file,
|
|
382
|
+
message: `${schemaSource.file} declares schema-level store, but runtime stores are configured in db.config.mjs.`,
|
|
383
|
+
hint: `Move this setting to resources.${resource}.store in db.config.mjs.`,
|
|
384
|
+
details: {
|
|
385
|
+
resource,
|
|
386
|
+
file: schemaSource.file,
|
|
387
|
+
property: 'store',
|
|
388
|
+
replacement: `resources.${resource}.store`,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function schemaParserDeprecatedDiagnostic(resource, schemaSource) {
|
|
393
|
+
return {
|
|
394
|
+
code: 'SCHEMA_PARSER_DEPRECATED',
|
|
395
|
+
severity: 'warn',
|
|
396
|
+
resource,
|
|
397
|
+
file: schemaSource.file,
|
|
398
|
+
message: `${schemaSource.file} declares parser, but file readers now belong on source: files(..., { read }).`,
|
|
399
|
+
hint: `Replace parser with source: files(pattern, { read: ${JSON.stringify(schemaSource.schema?.parser)} }).`,
|
|
400
|
+
details: {
|
|
401
|
+
resource,
|
|
402
|
+
file: schemaSource.file,
|
|
403
|
+
property: 'parser',
|
|
404
|
+
replacement: 'source.files.read',
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function normalizeSlash(value) {
|
|
409
|
+
return String(value).split(path.sep).join('/').split('\\').join('/');
|
|
410
|
+
}
|
|
411
|
+
function escapeRegExp(value) {
|
|
412
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
413
|
+
}
|
|
414
|
+
function mixedModeSchemaSeedDiagnostic(resource, dataSource, schemaSource) {
|
|
415
|
+
return {
|
|
416
|
+
code: 'SCHEMA_SEED_IGNORED_IN_MIXED_MODE',
|
|
417
|
+
severity: 'warn',
|
|
418
|
+
resource,
|
|
419
|
+
file: schemaSource.file,
|
|
420
|
+
message: `${schemaSource.file} includes seed records, but ${dataSource.file} provides seed data for "${resource}".`,
|
|
421
|
+
hint: `Remove "seed" from ${schemaSource.file}, or run async-db schema unbundle ${resource} to keep seed data in a separate fixture.`,
|
|
422
|
+
details: {
|
|
423
|
+
resource,
|
|
424
|
+
schemaFile: schemaSource.file,
|
|
425
|
+
dataFile: dataSource.file,
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function resourceAliasCollisionDiagnostics(resources) {
|
|
430
|
+
return resourceAliasCollisionGroups(resources).map((collision) => ({
|
|
431
|
+
code: 'RESOURCE_ALIAS_COLLISION',
|
|
432
|
+
severity: 'error',
|
|
433
|
+
message: `Resource aliases are ambiguous for "${collision.alias}": ${collision.resources.map((resource) => `"${resource}"`).join(' and ')} both resolve through ${collision.aliases.map((alias) => `"${alias}"`).join(', ')}.`,
|
|
434
|
+
hint: 'Rename one fixture or customize resource names so every camelCase and kebab-case alias maps to one resource.',
|
|
435
|
+
details: {
|
|
436
|
+
alias: collision.alias,
|
|
437
|
+
aliases: collision.aliases,
|
|
438
|
+
resources: collision.resources,
|
|
439
|
+
candidates: collision.candidates,
|
|
440
|
+
},
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type SchemaField = {
|
|
2
|
+
type?: string;
|
|
3
|
+
required?: boolean;
|
|
4
|
+
relation?: RawRelationDefinition;
|
|
5
|
+
};
|
|
6
|
+
type RawRelationDefinition = {
|
|
7
|
+
name?: string;
|
|
8
|
+
to?: string;
|
|
9
|
+
toField?: string;
|
|
10
|
+
cardinality?: string;
|
|
11
|
+
};
|
|
12
|
+
type RelationDefinition = {
|
|
13
|
+
name: string;
|
|
14
|
+
sourceResource: string;
|
|
15
|
+
sourceField: string;
|
|
16
|
+
targetResource: string;
|
|
17
|
+
targetField: string;
|
|
18
|
+
cardinality: string;
|
|
19
|
+
};
|
|
20
|
+
type SchemaResource = {
|
|
21
|
+
name: string;
|
|
22
|
+
kind?: string;
|
|
23
|
+
fields?: Record<string, SchemaField>;
|
|
24
|
+
relations?: RelationDefinition[];
|
|
25
|
+
seed?: Array<Record<string, unknown>>;
|
|
26
|
+
};
|
|
27
|
+
type SchemaDiagnostic = {
|
|
28
|
+
code: string;
|
|
29
|
+
severity: 'error' | 'warn';
|
|
30
|
+
resource: string;
|
|
31
|
+
field?: string;
|
|
32
|
+
message: string;
|
|
33
|
+
hint: string;
|
|
34
|
+
details: unknown;
|
|
35
|
+
};
|
|
36
|
+
export declare function validateProjectRelations(resources: SchemaResource[]): SchemaDiagnostic[];
|
|
37
|
+
export declare function relationsForResource(resource: SchemaResource): RelationDefinition[];
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { resolveResource } from '../../names.js';
|
|
2
|
+
const RELATION_SCALAR_FIELD_TYPES = new Set(['string', 'datetime', 'number', 'boolean', 'enum']);
|
|
3
|
+
export function validateProjectRelations(resources) {
|
|
4
|
+
const diagnostics = [];
|
|
5
|
+
const resourceMap = new Map(resources.map((resource) => [resource.name, resource]));
|
|
6
|
+
for (const resource of resources) {
|
|
7
|
+
for (const relation of resource.relations ?? []) {
|
|
8
|
+
const sourceField = resource.fields?.[relation.sourceField] ?? {};
|
|
9
|
+
const sourceFieldDiagnostic = relationSourceFieldDiagnostic(resource, relation, sourceField);
|
|
10
|
+
if (sourceFieldDiagnostic) {
|
|
11
|
+
diagnostics.push(sourceFieldDiagnostic);
|
|
12
|
+
}
|
|
13
|
+
const target = resolveResource(resourceMap, relation.targetResource).resource;
|
|
14
|
+
if (!target || target.kind !== 'collection') {
|
|
15
|
+
diagnostics.push({
|
|
16
|
+
code: 'SCHEMA_RELATION_TARGET_RESOURCE_MISSING',
|
|
17
|
+
severity: 'error',
|
|
18
|
+
resource: resource.name,
|
|
19
|
+
field: relation.sourceField,
|
|
20
|
+
message: `${resource.name} relation "${relation.name}" targets missing collection "${relation.targetResource}"`,
|
|
21
|
+
hint: 'Add the target collection fixture or update the relation.to value.',
|
|
22
|
+
details: relation,
|
|
23
|
+
});
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!(relation.targetField in (target.fields ?? {}))) {
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
code: 'SCHEMA_RELATION_TARGET_FIELD_MISSING',
|
|
29
|
+
severity: 'error',
|
|
30
|
+
resource: resource.name,
|
|
31
|
+
field: relation.sourceField,
|
|
32
|
+
message: `${resource.name} relation "${relation.name}" targets missing field "${relation.targetResource}.${relation.targetField}"`,
|
|
33
|
+
hint: 'Use an existing target field, usually the target collection id field.',
|
|
34
|
+
details: relation,
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (resource.kind !== 'collection') {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (sourceFieldDiagnostic) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const targetValues = new Set((Array.isArray(target.seed) ? target.seed : [])
|
|
45
|
+
.map((record) => record?.[relation.targetField])
|
|
46
|
+
.filter((value) => value !== undefined && value !== null && value !== '')
|
|
47
|
+
.map((value) => String(value)));
|
|
48
|
+
for (const [index, record] of resource.seed.entries()) {
|
|
49
|
+
const value = record?.[relation.sourceField];
|
|
50
|
+
if (value === undefined || value === null || value === '') {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (targetValues.has(String(value))) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
code: 'SCHEMA_RELATION_TARGET_MISSING',
|
|
58
|
+
severity: sourceField.required ? 'error' : 'warn',
|
|
59
|
+
resource: resource.name,
|
|
60
|
+
field: relation.sourceField,
|
|
61
|
+
message: `${resource.name} seed record ${index} field "${relation.sourceField}" links to missing ${relation.targetResource}.${relation.targetField} "${value}"`,
|
|
62
|
+
hint: `Add a matching ${relation.targetResource} record or update "${relation.sourceField}".`,
|
|
63
|
+
details: {
|
|
64
|
+
...relation,
|
|
65
|
+
value,
|
|
66
|
+
recordIndex: index,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return diagnostics;
|
|
73
|
+
}
|
|
74
|
+
function relationSourceFieldDiagnostic(resource, relation, sourceField) {
|
|
75
|
+
const sourceFieldType = sourceField?.type ?? 'unknown';
|
|
76
|
+
if (RELATION_SCALAR_FIELD_TYPES.has(sourceFieldType)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
code: 'SCHEMA_RELATION_SOURCE_FIELD_INVALID',
|
|
81
|
+
severity: 'error',
|
|
82
|
+
resource: resource.name,
|
|
83
|
+
field: relation.sourceField,
|
|
84
|
+
message: `${resource.name} relation "${relation.name}" source field "${relation.sourceField}" must be a scalar field, but found ${sourceFieldType}.`,
|
|
85
|
+
hint: 'Use a scalar id field for to-one relation metadata, such as string, number, boolean, datetime, or enum.',
|
|
86
|
+
details: {
|
|
87
|
+
relation,
|
|
88
|
+
sourceFieldType,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function relationsForResource(resource) {
|
|
93
|
+
if (resource.kind !== 'collection') {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return Object.entries(resource.fields ?? {})
|
|
97
|
+
.filter(([, field]) => field.relation)
|
|
98
|
+
.map(([fieldName, field]) => {
|
|
99
|
+
const relation = field.relation;
|
|
100
|
+
return {
|
|
101
|
+
name: relation.name,
|
|
102
|
+
sourceResource: resource.name,
|
|
103
|
+
sourceField: fieldName,
|
|
104
|
+
targetResource: relation.to,
|
|
105
|
+
targetField: relation.toField,
|
|
106
|
+
cardinality: relation.cardinality,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type ContextInput = Map<string, unknown> | Record<string, unknown> | null | undefined;
|
|
2
|
+
type ResolverContext = {
|
|
3
|
+
get(key: string): unknown;
|
|
4
|
+
has(key: string): boolean;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
type FieldResolverOptions = {
|
|
8
|
+
db?: unknown;
|
|
9
|
+
config?: unknown;
|
|
10
|
+
resource?: {
|
|
11
|
+
fields?: Record<string, unknown>;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
fieldName?: string;
|
|
15
|
+
cache?: Map<unknown, unknown>;
|
|
16
|
+
services?: Record<string, unknown>;
|
|
17
|
+
context?: unknown;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
};
|
|
20
|
+
type FieldResolverArgs = {
|
|
21
|
+
record?: unknown;
|
|
22
|
+
records?: unknown[];
|
|
23
|
+
value?: unknown;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
type RuntimeResource = {
|
|
27
|
+
name?: string;
|
|
28
|
+
kind?: string;
|
|
29
|
+
idField?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
type RuntimeRecord = Record<string, unknown>;
|
|
33
|
+
export declare function createResolverContext(internal?: ContextInput, provided?: ContextInput): ResolverContext;
|
|
34
|
+
export declare function callFieldResolver(resolver: (this: ResolverContext, args: FieldResolverArgs) => unknown, args: FieldResolverArgs, options?: FieldResolverOptions): Promise<unknown>;
|
|
35
|
+
export declare function valueFromResolveManyResult(values: unknown, resource: RuntimeResource, record: RuntimeRecord, index?: number): unknown;
|
|
36
|
+
export {};
|