@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,489 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { loadConfig } from '../config.js';
|
|
6
|
+
import { dbError, listChoices } from '../errors.js';
|
|
7
|
+
import { resolveResource, resourceAliasCollisionGroups } from '../names.js';
|
|
8
|
+
import { assertRecordMatchesResource, loadProjectSchema } from '../schema.js';
|
|
9
|
+
import { applyDefaultsToRecord } from '../sync.js';
|
|
10
|
+
import { applyDefaultsToSeed } from '../features/sync/defaults.js';
|
|
11
|
+
import { seedForRuntimeState } from '../features/sync/synthetic-seed.js';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
export async function openSqliteDb(options = {}) {
|
|
14
|
+
const config = await loadConfig(options);
|
|
15
|
+
const project = options.project ?? await loadProjectSchema(config);
|
|
16
|
+
const storage = options.storage ?? {};
|
|
17
|
+
const file = storage.file ?? options.file ?? path.join(config.stateDir, 'sqlite', 'db.sqlite');
|
|
18
|
+
const { DatabaseSync } = await importNodeSqlite();
|
|
19
|
+
if (file !== ':memory:') {
|
|
20
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
const database = new DatabaseSync(file);
|
|
23
|
+
migrateSqliteDb(database, project.resources);
|
|
24
|
+
return new SqliteDb(config, project.resources, database);
|
|
25
|
+
}
|
|
26
|
+
export function sqliteStore(options = {}) {
|
|
27
|
+
const databases = new WeakMap();
|
|
28
|
+
const writeQueues = new Map();
|
|
29
|
+
return ({ config, storeName }) => {
|
|
30
|
+
const file = resolveSqliteStoreFile(config, options.file);
|
|
31
|
+
const connection = openStoreDatabase(file, databases, config);
|
|
32
|
+
const database = connection.database;
|
|
33
|
+
migrateSqliteStore(database);
|
|
34
|
+
return {
|
|
35
|
+
name: storeName,
|
|
36
|
+
capabilities: sqliteStoreCapabilities,
|
|
37
|
+
statePath() {
|
|
38
|
+
return file === ':memory:' ? undefined : file;
|
|
39
|
+
},
|
|
40
|
+
async hydrate(resources) {
|
|
41
|
+
migrateSqliteStore(database);
|
|
42
|
+
for (const resource of resources) {
|
|
43
|
+
syncSqliteStoreResource(database, config, resource);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
readResource(resource, fallback) {
|
|
47
|
+
const row = database.prepare('SELECT value FROM "_db_resources" WHERE name = ?').get(resource.name);
|
|
48
|
+
return row ? JSON.parse(String(row.value)) : fallback;
|
|
49
|
+
},
|
|
50
|
+
writeResource(resource, value) {
|
|
51
|
+
writeSqliteStoreResource(database, resource, value);
|
|
52
|
+
},
|
|
53
|
+
withResourceWrite(resource, operation) {
|
|
54
|
+
const queueKey = `${file}:${resource.name}`;
|
|
55
|
+
const previous = writeQueues.get(queueKey) ?? Promise.resolve();
|
|
56
|
+
const current = previous.then(operation, operation);
|
|
57
|
+
const stored = current.catch(() => { });
|
|
58
|
+
writeQueues.set(queueKey, stored);
|
|
59
|
+
stored.finally(() => {
|
|
60
|
+
if (writeQueues.get(queueKey) === stored) {
|
|
61
|
+
writeQueues.delete(queueKey);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return current;
|
|
65
|
+
},
|
|
66
|
+
close() {
|
|
67
|
+
if (connection.closed) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
connection.closed = true;
|
|
71
|
+
database.close();
|
|
72
|
+
databases.delete(config);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function resolveSqliteStoreFile(config, file) {
|
|
78
|
+
if (file === ':memory:') {
|
|
79
|
+
return file;
|
|
80
|
+
}
|
|
81
|
+
if (file) {
|
|
82
|
+
return path.resolve(config.cwd, file);
|
|
83
|
+
}
|
|
84
|
+
return path.join(config.stateDir, 'runtime.sqlite');
|
|
85
|
+
}
|
|
86
|
+
export const sqliteStoreCapabilities = {
|
|
87
|
+
writable: true,
|
|
88
|
+
persistence: 'local-sqlite',
|
|
89
|
+
atomicity: 'resource',
|
|
90
|
+
liveEvents: true,
|
|
91
|
+
staticExport: false,
|
|
92
|
+
production: 'small-local',
|
|
93
|
+
};
|
|
94
|
+
export function migrateSqliteDb(database, resources) {
|
|
95
|
+
for (const resource of resources) {
|
|
96
|
+
if (resource.kind === 'collection') {
|
|
97
|
+
database.exec(createTableSql(resource));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
database.exec(`CREATE TABLE IF NOT EXISTS "_db_documents" (
|
|
101
|
+
"name" TEXT PRIMARY KEY,
|
|
102
|
+
"value" TEXT NOT NULL
|
|
103
|
+
) STRICT;`);
|
|
104
|
+
}
|
|
105
|
+
function migrateSqliteStore(database) {
|
|
106
|
+
database.exec(`CREATE TABLE IF NOT EXISTS "_db_resources" (
|
|
107
|
+
"name" TEXT PRIMARY KEY,
|
|
108
|
+
"kind" TEXT NOT NULL,
|
|
109
|
+
"source_hash" TEXT,
|
|
110
|
+
"value" TEXT NOT NULL
|
|
111
|
+
) STRICT;`);
|
|
112
|
+
}
|
|
113
|
+
function openStoreDatabase(file, databases, config) {
|
|
114
|
+
let connection = databases.get(config);
|
|
115
|
+
if (connection && !connection.closed) {
|
|
116
|
+
return connection;
|
|
117
|
+
}
|
|
118
|
+
const { DatabaseSync } = importNodeSqliteSync();
|
|
119
|
+
if (file !== ':memory:') {
|
|
120
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
connection = {
|
|
123
|
+
database: new DatabaseSync(file),
|
|
124
|
+
closed: false,
|
|
125
|
+
};
|
|
126
|
+
databases.set(config, connection);
|
|
127
|
+
return connection;
|
|
128
|
+
}
|
|
129
|
+
function syncSqliteStoreResource(database, config, resource) {
|
|
130
|
+
const row = database.prepare('SELECT source_hash FROM "_db_resources" WHERE name = ?').get(resource.name);
|
|
131
|
+
const sourceChanged = resource.dataHash && row?.source_hash !== resource.dataHash;
|
|
132
|
+
if (!row || sourceChanged) {
|
|
133
|
+
writeSqliteStoreResource(database, resource, applyDefaultsToSeed(seedForRuntimeState(resource, config), resource, config));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (config.defaults?.applyOnSafeMigration !== false) {
|
|
137
|
+
const current = JSON.parse(String(database.prepare('SELECT value FROM "_db_resources" WHERE name = ?').get(resource.name)?.value));
|
|
138
|
+
writeSqliteStoreResource(database, resource, applyDefaultsToSeed(current, resource, config));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function writeSqliteStoreResource(database, resource, value) {
|
|
142
|
+
database.prepare(`INSERT INTO "_db_resources" (name, kind, source_hash, value)
|
|
143
|
+
VALUES (?, ?, ?, ?)
|
|
144
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
145
|
+
kind = excluded.kind,
|
|
146
|
+
source_hash = excluded.source_hash,
|
|
147
|
+
value = excluded.value`).run(resource.name, resource.kind, resource.dataHash ?? null, JSON.stringify(value));
|
|
148
|
+
}
|
|
149
|
+
export class SqliteDb {
|
|
150
|
+
config;
|
|
151
|
+
resources;
|
|
152
|
+
database;
|
|
153
|
+
constructor(config, resources, database) {
|
|
154
|
+
this.config = config;
|
|
155
|
+
this.resources = new Map(resources.map((resource) => [resource.name, resource]));
|
|
156
|
+
assertNoResourceAliasCollisions(this.resources);
|
|
157
|
+
this.database = database;
|
|
158
|
+
}
|
|
159
|
+
collection(name) {
|
|
160
|
+
const resource = this.requireResource(name, 'collection');
|
|
161
|
+
return new SqliteDbCollection(this.config, resource, this.database);
|
|
162
|
+
}
|
|
163
|
+
document(name) {
|
|
164
|
+
const resource = this.requireResource(name, 'document');
|
|
165
|
+
return new SqliteDbDocument(this.config, resource, this.database);
|
|
166
|
+
}
|
|
167
|
+
resourceNames() {
|
|
168
|
+
return [...this.resources.keys()];
|
|
169
|
+
}
|
|
170
|
+
close() {
|
|
171
|
+
this.database.close();
|
|
172
|
+
}
|
|
173
|
+
requireResource(name, kind) {
|
|
174
|
+
const { resource, candidates } = resolveResource(this.resources, name);
|
|
175
|
+
if (!resource) {
|
|
176
|
+
throw dbError('SQLITE_UNKNOWN_RESOURCE', `Unknown SQLite db resource "${name}".`, {
|
|
177
|
+
status: 404,
|
|
178
|
+
hint: `Use one of: ${listChoices(this.resourceNames())}.`,
|
|
179
|
+
details: {
|
|
180
|
+
resource: name,
|
|
181
|
+
requestedResource: name,
|
|
182
|
+
normalizedCandidates: candidates,
|
|
183
|
+
availableResources: this.resourceNames(),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (resource.kind !== kind) {
|
|
188
|
+
throw dbError('SQLITE_RESOURCE_KIND_MISMATCH', `Resource "${name}" is a ${resource.kind}, not a ${kind}.`, {
|
|
189
|
+
status: 400,
|
|
190
|
+
hint: resource.kind === 'collection'
|
|
191
|
+
? `Use db.collection("${name}") for this resource.`
|
|
192
|
+
: `Use db.document("${name}") for this resource.`,
|
|
193
|
+
details: {
|
|
194
|
+
resource: name,
|
|
195
|
+
expectedKind: kind,
|
|
196
|
+
actualKind: resource.kind,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return resource;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function assertNoResourceAliasCollisions(resources) {
|
|
204
|
+
const collisions = resourceAliasCollisionGroups(resources);
|
|
205
|
+
if (collisions.length === 0) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const collision = collisions[0];
|
|
209
|
+
throw dbError('SQLITE_RESOURCE_ALIAS_COLLISION', `Resource aliases are ambiguous for "${collision.alias}".`, {
|
|
210
|
+
status: 400,
|
|
211
|
+
hint: 'Rename one resource so its camelCase and kebab-case aliases are unique.',
|
|
212
|
+
details: {
|
|
213
|
+
alias: collision.alias,
|
|
214
|
+
aliases: collision.aliases,
|
|
215
|
+
resources: collision.resources,
|
|
216
|
+
candidates: collision.candidates,
|
|
217
|
+
collisions,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export class SqliteDbCollection {
|
|
222
|
+
config;
|
|
223
|
+
resource;
|
|
224
|
+
database;
|
|
225
|
+
table;
|
|
226
|
+
constructor(config, resource, database) {
|
|
227
|
+
this.config = config;
|
|
228
|
+
this.resource = resource;
|
|
229
|
+
this.database = database;
|
|
230
|
+
this.table = quoteIdentifier(resource.name);
|
|
231
|
+
}
|
|
232
|
+
async all() {
|
|
233
|
+
return this.database.prepare(`SELECT * FROM ${this.table}`).all().map((row) => deserializeRow(this.resource, row));
|
|
234
|
+
}
|
|
235
|
+
async get(id) {
|
|
236
|
+
const idField = quoteIdentifier(this.resource.idField);
|
|
237
|
+
const row = this.database.prepare(`SELECT * FROM ${this.table} WHERE ${idField} = ?`).get(String(id));
|
|
238
|
+
return row ? deserializeRow(this.resource, row) : null;
|
|
239
|
+
}
|
|
240
|
+
async exists(id) {
|
|
241
|
+
const idField = quoteIdentifier(this.resource.idField);
|
|
242
|
+
const row = this.database.prepare(`SELECT 1 as found FROM ${this.table} WHERE ${idField} = ?`).get(String(id));
|
|
243
|
+
return Boolean(row);
|
|
244
|
+
}
|
|
245
|
+
async create(record) {
|
|
246
|
+
const fields = Object.keys(this.resource.fields);
|
|
247
|
+
const nextRecord = this.config.defaults?.applyOnCreate === false
|
|
248
|
+
? stripUnknownFields(this.resource, record)
|
|
249
|
+
: applyDefaultsToRecord(stripUnknownFields(this.resource, record), this.resource);
|
|
250
|
+
if (nextRecord[this.resource.idField] === undefined || nextRecord[this.resource.idField] === null || nextRecord[this.resource.idField] === '') {
|
|
251
|
+
nextRecord[this.resource.idField] = await this.nextId();
|
|
252
|
+
}
|
|
253
|
+
assertRecordMatchesResource(nextRecord, this.resource, this.config, {
|
|
254
|
+
source: `${this.resource.name} create body`,
|
|
255
|
+
});
|
|
256
|
+
const serialized = serializeRow(this.resource, nextRecord);
|
|
257
|
+
const columns = fields.map(quoteIdentifier).join(', ');
|
|
258
|
+
const placeholders = fields.map(() => '?').join(', ');
|
|
259
|
+
try {
|
|
260
|
+
this.database.prepare(`INSERT INTO ${this.table} (${columns}) VALUES (${placeholders})`).run(...fields.map((field) => serialized[field] ?? null));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (String(error.message).includes('UNIQUE')) {
|
|
264
|
+
throw dbError('SQLITE_DUPLICATE_ID', `Cannot create "${this.resource.name}" record because id "${nextRecord[this.resource.idField]}" already exists.`, {
|
|
265
|
+
status: 409,
|
|
266
|
+
hint: 'Use a unique id, or call patch/update if you intended to modify the existing record.',
|
|
267
|
+
details: {
|
|
268
|
+
resource: this.resource.name,
|
|
269
|
+
idField: this.resource.idField,
|
|
270
|
+
id: nextRecord[this.resource.idField],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
return nextRecord;
|
|
277
|
+
}
|
|
278
|
+
async update(id, patch) {
|
|
279
|
+
return this.patch(id, patch);
|
|
280
|
+
}
|
|
281
|
+
async patch(id, patch) {
|
|
282
|
+
const existing = await this.get(id);
|
|
283
|
+
if (!existing) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const nextRecord = stripUnknownFields(this.resource, {
|
|
287
|
+
...existing,
|
|
288
|
+
...(isRecord(patch) ? patch : {}),
|
|
289
|
+
[this.resource.idField]: existing[this.resource.idField],
|
|
290
|
+
});
|
|
291
|
+
assertRecordMatchesResource(nextRecord, this.resource, this.config, {
|
|
292
|
+
source: `${this.resource.name} patch body`,
|
|
293
|
+
});
|
|
294
|
+
const fields = Object.keys(this.resource.fields).filter((field) => field !== this.resource.idField);
|
|
295
|
+
const serialized = serializeRow(this.resource, nextRecord);
|
|
296
|
+
const assignments = fields.map((field) => `${quoteIdentifier(field)} = ?`).join(', ');
|
|
297
|
+
this.database.prepare(`UPDATE ${this.table} SET ${assignments} WHERE ${quoteIdentifier(this.resource.idField)} = ?`).run(...fields.map((field) => serialized[field] ?? null), String(id));
|
|
298
|
+
return nextRecord;
|
|
299
|
+
}
|
|
300
|
+
async delete(id) {
|
|
301
|
+
const result = this.database.prepare(`DELETE FROM ${this.table} WHERE ${quoteIdentifier(this.resource.idField)} = ?`).run(String(id));
|
|
302
|
+
return result.changes > 0;
|
|
303
|
+
}
|
|
304
|
+
async nextId() {
|
|
305
|
+
const rows = this.database.prepare(`SELECT ${quoteIdentifier(this.resource.idField)} as id FROM ${this.table}`).all();
|
|
306
|
+
const ids = rows.map((row) => String(row.id)).filter(Boolean);
|
|
307
|
+
const numeric = ids.map((id) => Number(id)).filter((id) => Number.isInteger(id) && id > 0);
|
|
308
|
+
let next = numeric.length > 0 ? Math.max(...numeric) + 1 : ids.length + 1;
|
|
309
|
+
while (ids.includes(String(next))) {
|
|
310
|
+
next += 1;
|
|
311
|
+
}
|
|
312
|
+
return String(next);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export class SqliteDbDocument {
|
|
316
|
+
config;
|
|
317
|
+
resource;
|
|
318
|
+
database;
|
|
319
|
+
constructor(config, resource, database) {
|
|
320
|
+
this.config = config;
|
|
321
|
+
this.resource = resource;
|
|
322
|
+
this.database = database;
|
|
323
|
+
}
|
|
324
|
+
async all() {
|
|
325
|
+
const row = this.database.prepare('SELECT value FROM "_db_documents" WHERE name = ?').get(this.resource.name);
|
|
326
|
+
return row ? JSON.parse(String(row.value)) : {};
|
|
327
|
+
}
|
|
328
|
+
async get(pointer = '') {
|
|
329
|
+
const document = await this.all();
|
|
330
|
+
return pointer ? getPointer(document, pointer) : document;
|
|
331
|
+
}
|
|
332
|
+
async put(value) {
|
|
333
|
+
const nextDocument = stripUnknownFields(this.resource, value);
|
|
334
|
+
assertRecordMatchesResource(nextDocument, this.resource, this.config, {
|
|
335
|
+
source: `${this.resource.name} document body`,
|
|
336
|
+
});
|
|
337
|
+
this.database.prepare('INSERT INTO "_db_documents" (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = excluded.value')
|
|
338
|
+
.run(this.resource.name, JSON.stringify(nextDocument));
|
|
339
|
+
return nextDocument;
|
|
340
|
+
}
|
|
341
|
+
async set(pointer, value) {
|
|
342
|
+
const document = await this.all();
|
|
343
|
+
setPointer(document, pointer, value);
|
|
344
|
+
await this.put(document);
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
async update(patch) {
|
|
348
|
+
const document = await this.all();
|
|
349
|
+
return this.put({ ...document, ...(isRecord(patch) ? patch : {}) });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function createTableSql(resource) {
|
|
353
|
+
const columns = Object.entries(resource.fields).map(([fieldName, field]) => {
|
|
354
|
+
const primary = fieldName === resource.idField ? ' PRIMARY KEY' : '';
|
|
355
|
+
const required = field.required && fieldName !== resource.idField ? ' NOT NULL' : '';
|
|
356
|
+
return ` ${quoteIdentifier(fieldName)} ${sqliteTypeForField(field)}${primary}${required}`;
|
|
357
|
+
});
|
|
358
|
+
return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(resource.name)} (
|
|
359
|
+
${columns.join(',\n')}
|
|
360
|
+
) STRICT;`;
|
|
361
|
+
}
|
|
362
|
+
function sqliteTypeForField(field) {
|
|
363
|
+
switch (field.type) {
|
|
364
|
+
case 'number':
|
|
365
|
+
return 'REAL';
|
|
366
|
+
case 'boolean':
|
|
367
|
+
return 'INTEGER';
|
|
368
|
+
case 'string':
|
|
369
|
+
case 'datetime':
|
|
370
|
+
case 'enum':
|
|
371
|
+
case 'object':
|
|
372
|
+
case 'array':
|
|
373
|
+
case 'unknown':
|
|
374
|
+
default:
|
|
375
|
+
return 'TEXT';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function stripUnknownFields(resource, record) {
|
|
379
|
+
const source = isRecord(record) ? record : {};
|
|
380
|
+
const next = {};
|
|
381
|
+
for (const fieldName of Object.keys(resource.fields ?? {})) {
|
|
382
|
+
if (source[fieldName] !== undefined) {
|
|
383
|
+
next[fieldName] = source[fieldName];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return next;
|
|
387
|
+
}
|
|
388
|
+
function serializeRow(resource, record) {
|
|
389
|
+
const row = {};
|
|
390
|
+
for (const [fieldName, field] of Object.entries(resource.fields)) {
|
|
391
|
+
const value = record[fieldName];
|
|
392
|
+
if (value === undefined) {
|
|
393
|
+
row[fieldName] = null;
|
|
394
|
+
}
|
|
395
|
+
else if (field.type === 'boolean') {
|
|
396
|
+
row[fieldName] = value ? 1 : 0;
|
|
397
|
+
}
|
|
398
|
+
else if (field.type === 'object' || field.type === 'array' || field.type === 'unknown') {
|
|
399
|
+
row[fieldName] = JSON.stringify(value);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
row[fieldName] = value;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return row;
|
|
406
|
+
}
|
|
407
|
+
function deserializeRow(resource, row) {
|
|
408
|
+
const record = {};
|
|
409
|
+
for (const [fieldName, field] of Object.entries(resource.fields)) {
|
|
410
|
+
const value = row[fieldName];
|
|
411
|
+
if (value === null || value === undefined) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (field.type === 'boolean') {
|
|
415
|
+
record[fieldName] = Boolean(value);
|
|
416
|
+
}
|
|
417
|
+
else if (field.type === 'object' || field.type === 'array' || field.type === 'unknown') {
|
|
418
|
+
record[fieldName] = typeof value === 'string' ? JSON.parse(value) : value;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
record[fieldName] = value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return record;
|
|
425
|
+
}
|
|
426
|
+
async function importNodeSqlite() {
|
|
427
|
+
try {
|
|
428
|
+
return await import('node:sqlite');
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
throw sqliteRuntimeUnavailableError(error);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function importNodeSqliteSync() {
|
|
435
|
+
try {
|
|
436
|
+
return require('node:sqlite');
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
throw sqliteRuntimeUnavailableError(error);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function sqliteRuntimeUnavailableError(error) {
|
|
443
|
+
const runtimeError = error;
|
|
444
|
+
return dbError('SQLITE_RUNTIME_UNAVAILABLE', 'SQLite store requires Node.js with node:sqlite support.', {
|
|
445
|
+
status: 500,
|
|
446
|
+
hint: 'Use Node.js 22.13 or newer for the SQLite store, or keep using the JSON store.',
|
|
447
|
+
details: {
|
|
448
|
+
parserMessage: runtimeError.message,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function quoteIdentifier(value) {
|
|
453
|
+
return `"${String(value).replaceAll('"', '""')}"`;
|
|
454
|
+
}
|
|
455
|
+
function getPointer(document, pointer) {
|
|
456
|
+
const parts = parsePointer(pointer);
|
|
457
|
+
let value = document;
|
|
458
|
+
for (const part of parts) {
|
|
459
|
+
if (value === null || value === undefined) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
value = value[part];
|
|
463
|
+
}
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
function setPointer(document, pointer, value) {
|
|
467
|
+
const parts = parsePointer(pointer);
|
|
468
|
+
let current = document;
|
|
469
|
+
while (parts.length > 1) {
|
|
470
|
+
const part = parts.shift();
|
|
471
|
+
if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) {
|
|
472
|
+
current[part] = {};
|
|
473
|
+
}
|
|
474
|
+
current = current[part];
|
|
475
|
+
}
|
|
476
|
+
current[parts[0]] = value;
|
|
477
|
+
}
|
|
478
|
+
function parsePointer(pointer) {
|
|
479
|
+
if (!pointer) {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
return String(pointer)
|
|
483
|
+
.split('/')
|
|
484
|
+
.slice(1)
|
|
485
|
+
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
486
|
+
}
|
|
487
|
+
function isRecord(value) {
|
|
488
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
489
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type ClientCacheOptions = boolean | {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
readPolicy?: string;
|
|
4
|
+
writePolicy?: string;
|
|
5
|
+
eventPolicy?: string | false;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
} | null | undefined;
|
|
8
|
+
type DbVitePluginOptions = Record<string, unknown> & {
|
|
9
|
+
apiBase?: string;
|
|
10
|
+
dataPath?: string | false;
|
|
11
|
+
rootRoutes?: boolean;
|
|
12
|
+
restBasePath?: string;
|
|
13
|
+
graphqlPath?: string;
|
|
14
|
+
trace?: unknown;
|
|
15
|
+
clientVirtualModule?: string | false | null;
|
|
16
|
+
clientImport?: string;
|
|
17
|
+
clientCache?: ClientCacheOptions;
|
|
18
|
+
server?: {
|
|
19
|
+
apiBase?: string;
|
|
20
|
+
dataPath?: string | false;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
type ViteMiddleware = (request: unknown, response: unknown, next: (error?: unknown) => unknown) => unknown;
|
|
25
|
+
type ViteDevServerLike = {
|
|
26
|
+
config?: {
|
|
27
|
+
logger?: {
|
|
28
|
+
warn?: (message: string) => unknown;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
middlewares: {
|
|
32
|
+
use(handler: ViteMiddleware): unknown;
|
|
33
|
+
};
|
|
34
|
+
httpServer?: {
|
|
35
|
+
once?(event: string, listener: () => void): unknown;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare function dbPlugin(options?: DbVitePluginOptions): {
|
|
39
|
+
name: string;
|
|
40
|
+
apply: string;
|
|
41
|
+
configureServer(server: ViteDevServerLike): Promise<void>;
|
|
42
|
+
resolveId(id: string): string;
|
|
43
|
+
load(id: string): string;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { openDb } from '../db.js';
|
|
2
|
+
import { serializeError } from '../errors.js';
|
|
3
|
+
import { createDbRequestHandler, createViewerEventHub, watchSourceDir } from '../server.js';
|
|
4
|
+
import { sendJson } from '../rest/handler.js';
|
|
5
|
+
const DEFAULT_VIRTUAL_CLIENT_MODULE = 'virtual:db/client';
|
|
6
|
+
const DEFAULT_CLIENT_IMPORT = '@async/db/client';
|
|
7
|
+
export function dbPlugin(options = {}) {
|
|
8
|
+
const routes = resolveViteRoutes(options);
|
|
9
|
+
const virtualModuleId = options.clientVirtualModule === false
|
|
10
|
+
? null
|
|
11
|
+
: options.clientVirtualModule ?? DEFAULT_VIRTUAL_CLIENT_MODULE;
|
|
12
|
+
const resolvedVirtualModuleId = virtualModuleId ? `\0${virtualModuleId}` : null;
|
|
13
|
+
return {
|
|
14
|
+
name: 'db:vite',
|
|
15
|
+
apply: 'serve',
|
|
16
|
+
async configureServer(server) {
|
|
17
|
+
const db = await openDb({
|
|
18
|
+
...dbOptions(options),
|
|
19
|
+
allowSourceErrors: true,
|
|
20
|
+
});
|
|
21
|
+
const events = createViewerEventHub();
|
|
22
|
+
const watcher = await watchSourceDir(db, events, {
|
|
23
|
+
warn(message) {
|
|
24
|
+
server.config?.logger?.warn?.(message);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const handler = createDbRequestHandler(db, {
|
|
28
|
+
...routes,
|
|
29
|
+
events,
|
|
30
|
+
trace: options.trace,
|
|
31
|
+
});
|
|
32
|
+
server.middlewares.use((request, response, next) => {
|
|
33
|
+
return handler(request, response, next).catch((error) => {
|
|
34
|
+
const statusError = error;
|
|
35
|
+
sendJson(response, statusError.status ?? 500, serializeError(statusError, 'SERVER_ERROR'));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
server.httpServer?.once?.('close', () => {
|
|
39
|
+
watcher.close();
|
|
40
|
+
events.close();
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
resolveId(id) {
|
|
44
|
+
return id === virtualModuleId ? resolvedVirtualModuleId : null;
|
|
45
|
+
},
|
|
46
|
+
load(id) {
|
|
47
|
+
if (id !== resolvedVirtualModuleId) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return renderVirtualClient(routes, options.clientImport ?? DEFAULT_CLIENT_IMPORT, options.clientCache);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function resolveViteRoutes(options) {
|
|
55
|
+
const apiBase = normalizeBasePath(options.apiBase ?? options.server?.apiBase ?? '/__db');
|
|
56
|
+
return {
|
|
57
|
+
apiBase,
|
|
58
|
+
dataPath: options.dataPath ?? options.server?.dataPath,
|
|
59
|
+
rootRoutes: options.rootRoutes === true,
|
|
60
|
+
restBasePath: normalizeBasePath(options.restBasePath ?? `${apiBase}/rest`),
|
|
61
|
+
graphqlPath: normalizeBasePath(options.graphqlPath ?? `${apiBase}/graphql`),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function renderVirtualClient(routes, clientImport, clientCache) {
|
|
65
|
+
const cacheOption = serializeVirtualClientCache(clientCache);
|
|
66
|
+
const defaultCacheLine = cacheOption ? ` cache: ${cacheOption},\n` : '';
|
|
67
|
+
return `import { createDbClient } from ${JSON.stringify(clientImport)};
|
|
68
|
+
|
|
69
|
+
export const client = createDbClient({
|
|
70
|
+
manifestPath: ${JSON.stringify(`${routes.apiBase}/manifest.json`)},
|
|
71
|
+
restBasePath: ${JSON.stringify(routes.restBasePath)},
|
|
72
|
+
restBatchPath: ${JSON.stringify(`${routes.apiBase}/batch`)},
|
|
73
|
+
graphqlPath: ${JSON.stringify(routes.graphqlPath)},
|
|
74
|
+
${defaultCacheLine}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export default client;
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
function dbOptions(options) {
|
|
81
|
+
const { apiBase, dataPath, rootRoutes, restBasePath, graphqlPath, trace, clientVirtualModule, clientImport, clientCache, ...db } = options;
|
|
82
|
+
return db;
|
|
83
|
+
}
|
|
84
|
+
function serializeVirtualClientCache(value) {
|
|
85
|
+
if (value === undefined || value === false) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (value === true) {
|
|
89
|
+
return 'true';
|
|
90
|
+
}
|
|
91
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const cache = {};
|
|
95
|
+
if (value.enabled !== undefined) {
|
|
96
|
+
cache.enabled = Boolean(value.enabled);
|
|
97
|
+
}
|
|
98
|
+
for (const key of ['readPolicy', 'writePolicy']) {
|
|
99
|
+
if (typeof value[key] === 'string') {
|
|
100
|
+
cache[key] = value[key];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (typeof value.eventPolicy === 'string' || value.eventPolicy === false) {
|
|
104
|
+
cache.eventPolicy = value.eventPolicy;
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify(cache);
|
|
107
|
+
}
|
|
108
|
+
function normalizeBasePath(value) {
|
|
109
|
+
const path = `/${String(value ?? '').replace(/^\/+/, '').replace(/\/+$/, '')}`;
|
|
110
|
+
return path === '/' ? '' : path;
|
|
111
|
+
}
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DbCustomStoreFactory } from './index.js';
|
|
2
|
+
|
|
3
|
+
export type JsonStoreCapabilities = {
|
|
4
|
+
writable: true;
|
|
5
|
+
persistence: 'local-file';
|
|
6
|
+
atomicity: 'resource';
|
|
7
|
+
liveEvents: true;
|
|
8
|
+
staticExport: false;
|
|
9
|
+
production: 'small-local';
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type JsonStateConfig = {
|
|
13
|
+
stateDir: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type JsonStateResource = {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type JsonFileStorage = {
|
|
21
|
+
kind: 'file';
|
|
22
|
+
root: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type JsonS3Storage = {
|
|
26
|
+
kind: 's3';
|
|
27
|
+
bucket: string;
|
|
28
|
+
prefix?: string;
|
|
29
|
+
client?: unknown;
|
|
30
|
+
encryption?: unknown;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type JsonStoreOptions = {
|
|
34
|
+
storage?: JsonFileStorage | JsonS3Storage;
|
|
35
|
+
durability?: 'current' | 'versioned' | string;
|
|
36
|
+
encryption?: unknown;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const jsonStoreCapabilities: JsonStoreCapabilities;
|
|
40
|
+
|
|
41
|
+
export function fileStorage(root: string): JsonFileStorage;
|
|
42
|
+
export function s3Storage(options: Omit<JsonS3Storage, 'kind'>): JsonS3Storage;
|
|
43
|
+
export function jsonStore(options?: JsonStoreOptions): DbCustomStoreFactory;
|
|
44
|
+
export function jsonStatePathForResource(config: JsonStateConfig, resource: string | JsonStateResource): string;
|
|
45
|
+
export function readJsonState<T>(filePath: string, fallback: T): Promise<T>;
|
|
46
|
+
export function writeJsonState(filePath: string, value: unknown): Promise<boolean>;
|
|
47
|
+
export function atomicWriteJson(filePath: string, value: unknown): Promise<boolean>;
|
|
48
|
+
export function withJsonStateWrite<T>(filePath: string, operation: () => T | Promise<T>): Promise<T>;
|
package/dist/json.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { atomicWriteJson, fileStorage, jsonStore, jsonRuntimeCapabilities as jsonStoreCapabilities, readJsonState, s3Storage, statePathForResource as jsonStatePathForResource, withJsonStateWrite, writeJsonState, } from './features/storage/json.js';
|
package/dist/jsonc.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseJsonc, stripJsonc } from './shared/jsonc.js';
|