Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ The defaults are designed to be useful without `jsondb.config.mjs`. When you do
| Fixture folder | `./db` | [Different fixture folder](#different-fixture-folder) | Keep fixtures in a package, example, or app-specific folder. |
| Runtime state behavior | `.jsondb`, mirror mode | [Mirror vs source mode](#mirror-vs-source-mode) | Keep source fixtures clean, or intentionally write generated ids back. |
| Importable generated types | `.jsondb/types/index.ts` | [Committed generated types](#committed-generated-types) | Let TypeScript imports work in CI and fresh checkouts before sync runs. |
| Importable schema manifest | Off | [Schema manifest output](#schema-manifest-output) | Generate runtime field metadata for model-driven admin/CMS forms. |
| Unknown fields in schema-backed data | Warn | [Schema strictness](#schema-strictness) | Move from permissive local data to stricter contracts. |
| Schema-only mock records | Off | [Generated schema seed data](#generated-schema-seed-data) | Create local records from schema when no fixture data exists yet. |
| Local latency | 30-100ms | [Mock delay and errors](#mock-delay-and-errors) | Disable it, use a fixed delay, or choose a different range. |
Expand Down Expand Up @@ -346,6 +347,7 @@ npm run db -- types --watch
npm run db -- types --out ./src/generated/jsondb.types.ts
npm run db -- schema
npm run db -- schema users
npm run db -- schema manifest --out ./src/generated/jsondb.schema.json
npm run db -- schema validate
npm run db -- create users '{"id":"u_2","name":"Grace Hopper","email":"grace@example.com"}'
npm run db -- serve
Expand All @@ -362,6 +364,7 @@ jsondb types --watch
jsondb types --out ./src/generated/jsondb.types.ts
jsondb schema
jsondb schema users
jsondb schema manifest --out ./src/generated/jsondb.schema.json
jsondb schema validate
jsondb create users '{"id":"u_2","name":"Grace Hopper","email":"grace@example.com"}'
jsondb serve
Expand Down Expand Up @@ -800,6 +803,23 @@ export default defineConfig({
emitComments: true,
},

schemaOutFile: './src/generated/jsondb.schema.json',
schemaManifest: {
customizeField({ fieldName, defaultManifest }) {
if (fieldName.endsWith('Markdown')) {
return {
...defaultManifest,
ui: {
...defaultManifest.ui,
component: 'markdown',
},
};
}

return defaultManifest;
},
},

schema: {
unknownFields: 'warn', // "allow" | "warn" | "error"
},
Expand Down Expand Up @@ -866,6 +886,56 @@ export default defineConfig({
});
```

### Schema Manifest Output

Use `schemaOutFile` when a local admin or CMS UI needs runtime schema metadata instead of hand-coded forms:

```js
import { defineConfig } from 'jsondb/config';

export default defineConfig({
schemaOutFile: './src/generated/jsondb.schema.json',
});
```

`jsondb sync` writes the manifest when `schemaOutFile` is set. You can also generate it directly:

```bash
jsondb schema manifest --out ./src/generated/jsondb.schema.json
```

The JSON manifest groups `collections` and `documents`, includes normalized field metadata such as `type`, `required`, `nullable`, `default`, `values`, nested `fields`, array `items`, and `relation`, and adds inferred `ui` defaults. Defaults are metadata only; they do not change fixtures, seed data, runtime state, REST, or GraphQL behavior.

Override or omit field output with a visitor hook:

```js
import { defineConfig } from 'jsondb/config';

export default defineConfig({
schemaManifest: {
customizeField({ fieldName, resourceName, file, path, defaultManifest }) {
if (resourceName === 'users' && fieldName === 'passwordHash') {
return null;
}

if (fieldName.endsWith('Markdown')) {
return {
...defaultManifest,
ui: {
...defaultManifest.ui,
component: 'markdown',
section: file,
orderKey: path,
},
};
}

return defaultManifest;
},
},
});
```

### Schema Strictness

Use strict unknown-field checks when schema-backed fixtures should reject drift:
Expand Down
105 changes: 104 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,23 @@ Projects can customize the output location:
export default {
dbDir: './db',
stateDir: './.jsondb',
schemaOutFile: './src/generated/jsondb.schema.json',

schemaManifest: {
customizeField({ fieldName, defaultManifest }) {
if (fieldName.endsWith('Markdown')) {
return {
...defaultManifest,
ui: {
...defaultManifest.ui,
component: 'markdown',
},
};
}

return defaultManifest;
},
},

types: {
enabled: true,
Expand Down Expand Up @@ -615,10 +632,11 @@ Add schema commands:
```bash
jsondb schema
jsondb schema users
jsondb schema manifest --out ./src/generated/jsondb.schema.json
jsondb schema validate
```

`jsondb sync` should also regenerate types.
`jsondb sync` should also regenerate types and should write the committed schema manifest when `schemaOutFile` is configured.

Expected output:

Expand All @@ -628,6 +646,7 @@ Loaded db/posts.json
Generated .jsondb/schema.generated.json
Generated .jsondb/types/index.ts
Generated src/generated/jsondb.types.ts
Generated src/generated/jsondb.schema.json
Synced runtime mirror
```

Expand Down Expand Up @@ -997,6 +1016,90 @@ export type User = {

Use schema field descriptions to emit JSDoc comments.

## Schema manifest output and model-driven admin UIs

Add optional JSON schema manifest generation for local-first admin/CMS UIs that render forms from JSONDB models instead of duplicating per-resource form configuration.

This is separate from `.jsondb/schema.generated.json`. The existing generated schema file remains runtime/server metadata and may include diagnostics, source paths, seeds, REST route lists, and GraphQL SDL. The committed manifest is a small importable artifact for applications.

Configure it with:

```js
export default {
schemaOutFile: './src/generated/jsondb.schema.json',
};
```

When `schemaOutFile` is set, `jsondb sync` writes the manifest. The CLI can also write one directly:

```bash
jsondb schema manifest --out ./src/generated/jsondb.schema.json
```

The manifest should have this top-level shape:

```json
{
"version": 1,
"collections": {},
"documents": {}
}
```

Each resource entry should include `kind`, `name`, `idField` for collections, optional `description`, and `fields`. Each field should include normalized field metadata such as `type`, `required`, `nullable`, `default`, `values`, nested object `fields`, array `items`, `relation`, constraints, and inferred `ui` defaults.

The manifest must not include seed records, source hashes, source paths, runtime state, diagnostics, REST route lists, or GraphQL SDL.

Default UI inference should be deterministic and safe:

```txt
boolean -> toggle
small enum -> radio
larger enum -> select
email-like field name -> email
url-like field name -> url
image/avatar/photo-like field name -> image
description/body/content/notes/bio/markdown-like field name -> textarea
array<string> -> tags
array<enum> -> multiSelect
object with declared fields -> fieldset
open object or unknown field -> json
relation field -> relationSelect with optionsFrom
collection id field -> readonly
```

Manifest defaults are metadata only. They must not change fixtures, seed data, runtime state, validation, REST, or GraphQL behavior.

Apps can customize or omit field entries with a visitor hook:

```js
export default {
schemaManifest: {
customizeField({ field, fieldName, resource, resourceName, path, file, sourceFile, defaultManifest }) {
if (resourceName === 'users' && fieldName === 'passwordHash') {
return null;
}

if (fieldName.endsWith('Markdown')) {
return {
...defaultManifest,
ui: {
...defaultManifest.ui,
component: 'markdown',
},
};
}

return defaultManifest;
},
},
};
```

The visitor return value must be JSON-serializable. Functions, classes, symbols, bigint values, non-finite numbers, and non-plain objects should fail generation with a diagnostic that includes resource and field path. Returning `null` omits the field from the manifest.

The intended first use is permissioned admin CRUD for resources such as dashboards, users, and permission policies. Admin screens can map manifest field metadata to reusable create/edit/view components while policy checks decide whether fields are hidden, readonly, or editable for a given session.

Support schema-only fixtures.

The package should accept these source formats:
Expand Down
11 changes: 11 additions & 0 deletions jsondb.config.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ export default defineConfig({
// Runtime output folder. Defaults to './.jsondb'.
stateDir: './.jsondb',

// Optional committed JSON schema manifest for model-driven admin/CMS UIs.
// Generated during `jsondb sync` when set.
schemaOutFile: null,

// Optional visitor hook for changing or omitting generated manifest fields.
schemaManifest: {
customizeField({ defaultManifest }) {
return defaultManifest;
},
},

// mirror: keep source fixtures unchanged and write app edits to .jsondb/state.
// source: write generated ids back to plain .json fixtures when needed.
mode: 'mirror',
Expand Down
19 changes: 19 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { runJsonDbDoctor } from './doctor.js';
import { openJsonFixtureDb } from './db.js';
import { generateHonoStarter } from './generate/hono.js';
import { loadProjectSchema } from './schema.js';
import { generateSchemaManifest } from './schema-manifest.js';
import { startJsonDbServer } from './server.js';
import { syncJsonFixtureDb } from './sync.js';
import { generateTypes } from './types.js';
Expand Down Expand Up @@ -109,6 +110,23 @@ async function runTypesOnce(config, args) {
async function runSchema(config, args) {
const project = await loadProjectSchema(config);

if (args[0] === 'manifest') {
const result = await generateSchemaManifest(config, {
project,
outFile: valueAfter(args, '--out'),
});

if (result.outFiles.length === 0) {
console.log(result.content);
return;
}

for (const filePath of result.outFiles) {
console.log(`Generated ${path.relative(config.cwd, filePath)}`);
}
return;
}

if (args[0] === 'validate') {
for (const diagnostic of project.diagnostics) {
printDiagnostic(diagnostic);
Expand Down Expand Up @@ -240,6 +258,7 @@ Usage:
jsondb sync
jsondb types [--watch] [--out <file>]
jsondb schema [resource]
jsondb schema manifest [--out <file>]
jsondb schema validate
jsondb doctor [--strict] [--json]
jsondb check [--strict] [--json]
Expand Down
6 changes: 6 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const DEFAULT_CONFIG = {
dbDir: './db',
sourceDir: './db',
stateDir: './.jsondb',
schemaOutFile: null,
schemaManifest: {},
mode: 'mirror',
types: {
enabled: true,
Expand Down Expand Up @@ -99,6 +101,10 @@ export async function loadConfig(options = {}) {
merged.types.commitOutFile = resolveFrom(cwd, merged.types.commitOutFile);
}

if (merged.schemaOutFile) {
merged.schemaOutFile = resolveFrom(cwd, merged.schemaOutFile);
}

merged.forks = normalizeForks(merged, merged.forks);

return merged;
Expand Down
22 changes: 22 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ export type JsonDbGeneratedTypesOptions = {
exportRuntimeHelpers?: boolean;
};

export type JsonDbSchemaManifestFieldContext = {
field: Record<string, unknown>;
fieldName: string;
resource: Record<string, unknown>;
resourceName: string;
path: string;
file: string | null;
sourceFile: string | null;
defaultManifest: Record<string, unknown>;
};

export type JsonDbSchemaManifestOptions = {
/** Customize or omit generated field manifest entries. Return null to omit a field. */
customizeField?: (context: JsonDbSchemaManifestFieldContext) => Record<string, unknown> | null;
};

export type JsonDbOptions = {
/** Project root used to resolve relative config paths. Defaults to process.cwd(). */
cwd?: string;
Expand All @@ -31,6 +47,10 @@ export type JsonDbOptions = {
sourceDir?: string;
/** Generated runtime output folder. Defaults to "./.jsondb". */
stateDir?: string;
/** Optional committed generated JSON schema manifest for admin/CMS UI generation. */
schemaOutFile?: string | null;
/** Optional visitor hooks for customizing generated schema manifest output. */
schemaManifest?: JsonDbSchemaManifestOptions;
/** "mirror" keeps source fixtures unchanged; "source" may write generated ids back to plain .json fixtures. */
mode?: 'mirror' | 'source';
/** Run sync automatically when opening the package API. */
Expand Down Expand Up @@ -261,6 +281,8 @@ export function runJsonDbDoctor(config: JsonDbOptions): Promise<JsonDbDoctorResu
export function startJsonDbServer(options?: JsonDbOptions & { host?: string; port?: number }): Promise<JsonDbServer>;
export function syncJsonFixtureDb(config: JsonDbOptions, options?: { allowErrors?: boolean }): Promise<unknown>;
export function generateTypes(config: JsonDbOptions, options?: { outFile?: string }): Promise<{ content: string; outFiles: string[] }>;
export function generateSchemaManifest(config: JsonDbOptions, options?: { outFile?: string }): Promise<{ manifest: unknown; content: string; outFiles: string[] }>;
export function renderSchemaManifest(resources: unknown[], config?: JsonDbOptions): unknown;
export function generateHonoStarter(
config: JsonDbOptions,
options?: {
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { runJsonDbDoctor } from './doctor.js';
export { executeGraphql, executeGraphqlBatch, parseGraphql } from './graphql/index.js';
export { generateHonoStarter, renderHonoStarter } from './generate/hono.js';
export { loadProjectSchema, makeGeneratedSchema } from './schema.js';
export { generateSchemaManifest, renderSchemaManifest } from './schema-manifest.js';
export { createJsonDbRequestHandler, startJsonDbServer } from './server.js';
export { syncJsonFixtureDb } from './sync.js';
export { generateTypes, renderTypes } from './types.js';
Loading
Loading