@localess/schema 4.0.0-dev.20260905071322 → 4.0.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/SKILL.md +203 -203
- package/dist/define.d.ts +3 -3
- package/dist/infer.d.ts +10 -6
- package/package.json +57 -57
package/SKILL.md
CHANGED
|
@@ -1,203 +1,203 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: localess-schema
|
|
3
|
-
description: Programmatic Localess schema definitions in TypeScript, with pure type inference of content types (no codegen). Use when defining ROOT/NODE/ENUM schemas in code, deriving content types from those definitions, or validating/exporting a schema config before pushing it to a Localess space via `@localess/cli`.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# @localess/schema
|
|
7
|
-
|
|
8
|
-
Define Localess schemas (ROOT content types, NODE nested components, ENUM
|
|
9
|
-
option sets) in TypeScript and derive content types from them by pure type
|
|
10
|
-
inference — no build step, no generated `.d.ts` file. Zero external
|
|
11
|
-
dependencies: its only dependency is `@localess/model`, the shared,
|
|
12
|
-
dependency-free domain-model package (ADR 009).
|
|
13
|
-
|
|
14
|
-
`@localess/cli`'s `schema pull`/`push`/`diff`/`validate` commands sync
|
|
15
|
-
definitions written with this package to and from a Localess space. See that
|
|
16
|
-
package's docs for the sync workflow; this package only defines, validates,
|
|
17
|
-
infers, and exports — it does no I/O.
|
|
18
|
-
|
|
19
|
-
## Quick start
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { defineConfig, defineEnum, defineSchema } from '@localess/schema';
|
|
23
|
-
import type { InferContentData } from '@localess/schema';
|
|
24
|
-
|
|
25
|
-
const ButtonType = defineEnum({
|
|
26
|
-
id: 'ButtonType',
|
|
27
|
-
displayName: 'Button Type',
|
|
28
|
-
values: [
|
|
29
|
-
{ name: 'Primary', value: 'primary' },
|
|
30
|
-
{ name: 'Secondary', value: 'secondary' },
|
|
31
|
-
],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const Button = defineSchema({
|
|
35
|
-
id: 'Button',
|
|
36
|
-
type: 'NODE',
|
|
37
|
-
displayName: 'Button',
|
|
38
|
-
previewField: 'label',
|
|
39
|
-
fields: [
|
|
40
|
-
{ name: 'label', kind: 'TEXT', required: true, translatable: true, maxLength: 50 },
|
|
41
|
-
{ name: 'type', kind: 'OPTION', source: ButtonType }, // by-value ref, normalized to 'ButtonType'
|
|
42
|
-
{ name: 'icon', kind: 'ASSET', fileTypes: ['IMAGE'] },
|
|
43
|
-
],
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const Page = defineSchema({
|
|
47
|
-
id: 'Page',
|
|
48
|
-
type: 'ROOT',
|
|
49
|
-
fields: [
|
|
50
|
-
{ name: 'title', kind: 'TEXT', required: true },
|
|
51
|
-
{ name: 'blocks', kind: 'SCHEMAS', schemas: [Button] },
|
|
52
|
-
],
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
export const config = defineConfig({ schemas: [Page, Button, ButtonType] });
|
|
56
|
-
|
|
57
|
-
// Content = { _id: string; _schema: 'Page'; title: string; blocks?: ButtonContent[] }
|
|
58
|
-
export type Content = InferContentData<typeof config>;
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## API reference
|
|
62
|
-
|
|
63
|
-
| Export | Purpose |
|
|
64
|
-
|---|---|
|
|
65
|
-
| `defineEnum(definition)` | Define an ENUM schema. Identity function; injects `type: 'ENUM'`. |
|
|
66
|
-
| `defineSchema(definition)` | Define a ROOT or NODE schema. Normalizes by-value refs (`source`, `schemas`) to id strings. Throws on duplicate field names. |
|
|
67
|
-
| `defineField(field)` | Define a single field, narrowed by `kind`. Optional; catches a stray property from the wrong kind at the call site, unlike a bare field literal. Identity function. |
|
|
68
|
-
| `defineConfig({ schemas })` | Register the full schema list — the unit the CLI loads and inference resolves against. Throws on duplicate schema ids. |
|
|
69
|
-
| `validate(config)` | Non-throwing `{ ok, issues }` — ID/name patterns, reserved names, length limits, reference resolution. |
|
|
70
|
-
| `toSchemaExport(config)` | Pure mapping to the wire format (`SchemaExport[]`) the Localess API accepts/returns. |
|
|
71
|
-
| `InferContentData<C>` | Union of every ROOT schema's content type in config `C`. |
|
|
72
|
-
| `InferContent<S, C>` | Content type of one schema definition `S`, resolved against config `C`. |
|
|
73
|
-
| `InferEnum<E>` | Literal union of an enum definition's values. |
|
|
74
|
-
|
|
75
|
-
### Exported types
|
|
76
|
-
|
|
77
|
-
Everything below is `export type` — importable with `import type { ... } from '@localess/schema'`.
|
|
78
|
-
|
|
79
|
-
| Group | Types |
|
|
80
|
-
|---|---|
|
|
81
|
-
| Authoring (from `define.ts`) | `EnumDefinition` (result of `defineEnum`), `ComponentDefinition` (result of `defineSchema`), `SchemaDefinition` (`ComponentDefinition \| EnumDefinition`), `LocalessSchemaConfig` (`{ schemas: readonly SchemaDefinition[] }` — what `validate`/`toSchemaExport` accept), `SchemaFieldInput` (a field as authored: `source`/`schemas` accept by-value refs), `EnumDefinitionInput`, `ComponentDefinitionInput` |
|
|
82
|
-
| Inference (from `infer.ts`) | `InferContentData`, `InferContent`, `InferEnum` |
|
|
83
|
-
| Validation (from `validate.ts`) | `ValidationIssue` (`{ severity: 'error' \| 'warning'; code; path; message }`), `ValidationResult` (`{ ok; issues }`) |
|
|
84
|
-
| Wire model (re-exported from `@localess/model`) | `SchemaType`, `SchemaFieldKind`, `AssetFileType`, `SchemaEnumValue`, `SchemaFieldBase`, `SchemaField`, the 18 per-kind interfaces (`SchemaFieldText`, `SchemaFieldTextarea`, `SchemaFieldRichText`, `SchemaFieldMarkdown`, `SchemaFieldNumber`, `SchemaFieldColor`, `SchemaFieldDate`, `SchemaFieldDateTime`, `SchemaFieldBoolean`, `SchemaFieldOption`, `SchemaFieldOptions`, `SchemaFieldLink`, `SchemaFieldReference`, `SchemaFieldReferences`, `SchemaFieldAsset`, `SchemaFieldAssets`, `SchemaFieldSchema`, `SchemaFieldSchemas`), `SchemaComponentExport`, `SchemaEnumExport`, `SchemaExport` |
|
|
85
|
-
| Content values (re-exported from `@localess/model`) | `ContentAsset`, `ContentLink`, `ContentReference`, `ContentRichText` |
|
|
86
|
-
|
|
87
|
-
## `defineField`
|
|
88
|
-
|
|
89
|
-
Optional. Wraps a single field so TypeScript catches a stray property from
|
|
90
|
-
the wrong `kind` at the call site — something a bare field literal inside
|
|
91
|
-
`defineSchema({ fields: [...] })` cannot do:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
import { defineField } from '@localess/schema';
|
|
95
|
-
|
|
96
|
-
defineField({ name: 'amount', kind: 'NUMBER', maxLength: 5 });
|
|
97
|
-
// ^ compile error: maxLength is not valid on a NUMBER field
|
|
98
|
-
|
|
99
|
-
defineField({ name: 'amount', kind: 'NUMBER', minValue: 0 }); // OK
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
`defineSchema`'s `fields` array accepts raw literals and `defineField(...)`
|
|
103
|
-
results interchangeably — by-value ref normalization (`source`, `schemas`)
|
|
104
|
-
still happens exclusively in `defineSchema`, regardless of a field's origin.
|
|
105
|
-
|
|
106
|
-
## By-value references
|
|
107
|
-
|
|
108
|
-
`OPTION`/`OPTIONS` fields accept an enum definition (or a string id) in
|
|
109
|
-
`source`; `SCHEMA`/`SCHEMAS` fields accept schema definitions (or string ids)
|
|
110
|
-
in `schemas`. Both are normalized to string ids at runtime by `defineSchema`,
|
|
111
|
-
matching the wire format exactly — but the *type* keeps the literal id, so
|
|
112
|
-
`InferContent` resolves `OPTION` fields to the referenced enum's literal
|
|
113
|
-
value union instead of plain `string`, and `SCHEMA`/`SCHEMAS` fields to the
|
|
114
|
-
allowed schemas' content types (or every `NODE` schema in the config when the
|
|
115
|
-
`schemas` list is omitted).
|
|
116
|
-
|
|
117
|
-
Fallbacks when a reference can't be resolved against the config:
|
|
118
|
-
|
|
119
|
-
- `OPTION`/`OPTIONS` whose `source` id is not in the config → `string` (or
|
|
120
|
-
`string[]`), mirroring the CLI's `type generate` behavior.
|
|
121
|
-
- `InferEnum` of an enum with no `values` → `string`.
|
|
122
|
-
- Unrestricted `SCHEMA`/`SCHEMAS` (no `schemas` list) in a config with no
|
|
123
|
-
`NODE` schemas → `{ _id: string; _schema: string }` (or an array of it).
|
|
124
|
-
- `InferContentData` of a config with no `ROOT` schema → `never`.
|
|
125
|
-
|
|
126
|
-
## Field kinds
|
|
127
|
-
|
|
128
|
-
Every field carries `name`, `kind`, and the base optional properties
|
|
129
|
-
(`displayName`, `required`, `description`, `defaultValue`, `translatable`).
|
|
130
|
-
`required: true` fields are non-optional keys on the inferred content type;
|
|
131
|
-
everything else is optional.
|
|
132
|
-
|
|
133
|
-
| Kind | Extra properties | Inferred type |
|
|
134
|
-
|---|---|---|
|
|
135
|
-
| `TEXT`, `TEXTAREA`, `MARKDOWN` | `minLength?`, `maxLength?` | `string` |
|
|
136
|
-
| `RICH_TEXT` | `minLength?`, `maxLength?` | `ContentRichText` |
|
|
137
|
-
| `NUMBER` | `minValue?`, `maxValue?` | `number` |
|
|
138
|
-
| `COLOR`, `DATE`, `DATETIME` | — | `string` |
|
|
139
|
-
| `BOOLEAN` | — | `boolean` |
|
|
140
|
-
| `OPTION` | `source` (required; enum id or `defineEnum` result) | literal union of the referenced enum's values |
|
|
141
|
-
| `OPTIONS` | `source` (required), `minValues?`, `maxValues?` | that union, as an array |
|
|
142
|
-
| `LINK` | — | `ContentLink` |
|
|
143
|
-
| `REFERENCE` / `REFERENCES` | `path?` | `ContentReference` / `ContentReference[]` |
|
|
144
|
-
| `ASSET` / `ASSETS` | `fileTypes?: AssetFileType[]`, `fileType?: AssetFileType` | `ContentAsset` / `ContentAsset[]` |
|
|
145
|
-
| `SCHEMA` / `SCHEMAS` | `schemas?` (allowed ids or `defineSchema` results; unrestricted when absent) | allowed schemas' content type / array of it |
|
|
146
|
-
|
|
147
|
-
`AssetFileType` is `'ANY' | 'IMAGE' | 'VIDEO' | 'TEXT' | 'AUDIO' | 'APPLICATION'`.
|
|
148
|
-
|
|
149
|
-
`ContentAsset`, `ContentLink`, `ContentReference`, and `ContentRichText` are
|
|
150
|
-
re-exported from `@localess/model`, the shared domain-model package (see
|
|
151
|
-
ADR 009).
|
|
152
|
-
|
|
153
|
-
## `validate(config)`
|
|
154
|
-
|
|
155
|
-
Non-throwing. Returns `{ ok, issues }`; `ok` is `false` only when at least
|
|
156
|
-
one `error`-severity issue is present (the `ValidationIssue` type allows
|
|
157
|
-
`'warning'`, but every rule currently implemented reports `error`). Every
|
|
158
|
-
schema in the config is checked, not just the first. Rules and their `code`:
|
|
159
|
-
|
|
160
|
-
| Code | Rule |
|
|
161
|
-
|---|---|
|
|
162
|
-
| `schema/invalid-id` | Schema id must match `/^[a-zA-Z][a-zA-Z0-9]+$/` and be 2-50 characters. |
|
|
163
|
-
| `schema/reserved-id` | Schema id must not be (case-insensitively) `Translations`, `Links`, `ContentMetadata`, `ContentReference`, `ContentRichText`, `ContentLink`, `ContentData`, `ContentAsset`, or `Content`. |
|
|
164
|
-
| `schema/display-name-too-long` | Schema `displayName` ≤ 50 characters. |
|
|
165
|
-
| `schema/description-too-long` | Schema `description` ≤ 250 characters. |
|
|
166
|
-
| `schema/invalid-label` | Each label 2-50 characters, no spaces. |
|
|
167
|
-
| `enum/invalid-value-name` | ENUM value `name` 1-50 characters. |
|
|
168
|
-
| `enum/invalid-value` | ENUM `value` 1-50 characters matching `/^[a-zA-Z]$\|^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z0-9]$/`. |
|
|
169
|
-
| `schema/unknown-preview-field` | `previewField` must name a field of the same schema. |
|
|
170
|
-
| `field/invalid-name` | Field name must match `/^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]$/` (camelCase), be 2-30 characters, and not contain `_i18n_`. |
|
|
171
|
-
| `field/reserved-name` | Field name must not be (case-insensitively) `_id` or `_schema`. |
|
|
172
|
-
| `field/display-name-too-long` | Field `displayName` ≤ 30 characters. |
|
|
173
|
-
| `field/description-too-long` | Field `description` ≤ 250 characters. |
|
|
174
|
-
| `field/default-value-too-long` | Field `defaultValue` ≤ 250 characters. |
|
|
175
|
-
| `field/unresolved-source` | `OPTION`/`OPTIONS` `source` must be the id of a schema in the config. |
|
|
176
|
-
| `field/source-not-enum` | ...and that schema must be an `ENUM`. |
|
|
177
|
-
| `field/unresolved-schema-ref` | Every `SCHEMA`/`SCHEMAS` `schemas` entry must be the id of a schema in the config. |
|
|
178
|
-
| `field/schema-ref-is-enum` | ...and that schema must be `ROOT` or `NODE`, not `ENUM`. |
|
|
179
|
-
|
|
180
|
-
`validate` does not check for stray properties from the wrong field kind —
|
|
181
|
-
use `defineField` for that (below). Duplicate field names and duplicate
|
|
182
|
-
schema ids are not validation issues either: `defineSchema`/`defineConfig`
|
|
183
|
-
throw on them as programming errors.
|
|
184
|
-
|
|
185
|
-
## `toSchemaExport(config)`
|
|
186
|
-
|
|
187
|
-
Pure mapping to `SchemaExport[]`, preserving schema order. Strips
|
|
188
|
-
`undefined`-valued keys from each schema and from the items of its array
|
|
189
|
-
properties (`fields`, `values`), so the result round-trips through JSON
|
|
190
|
-
unchanged. Performs no I/O.
|
|
191
|
-
|
|
192
|
-
## Known limitation (mitigated by `defineField`)
|
|
193
|
-
|
|
194
|
-
TypeScript's excess-property check doesn't apply to object literals inside
|
|
195
|
-
an array passed through a `const`-inferred generic parameter (only to
|
|
196
|
-
literals checked directly against a declared type). A stray property from
|
|
197
|
-
the wrong field kind — e.g. `maxLength` on a `NUMBER` field — inside a bare
|
|
198
|
-
field literal in `defineSchema({ fields: [...] })` is therefore not flagged
|
|
199
|
-
at the call site. Wrap the field in `defineField(...)` instead to get that
|
|
200
|
-
check while keeping full literal-type preservation (see "`defineField`"
|
|
201
|
-
above). Missing required properties (e.g. omitting `source` on `OPTION`) are
|
|
202
|
-
still caught either way. `validate()` and the Localess backend's schema
|
|
203
|
-
validation don't check for this either today.
|
|
1
|
+
---
|
|
2
|
+
name: localess-schema
|
|
3
|
+
description: Programmatic Localess schema definitions in TypeScript, with pure type inference of content types (no codegen). Use when defining ROOT/NODE/ENUM schemas in code, deriving content types from those definitions, or validating/exporting a schema config before pushing it to a Localess space via `@localess/cli`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @localess/schema
|
|
7
|
+
|
|
8
|
+
Define Localess schemas (ROOT content types, NODE nested components, ENUM
|
|
9
|
+
option sets) in TypeScript and derive content types from them by pure type
|
|
10
|
+
inference — no build step, no generated `.d.ts` file. Zero external
|
|
11
|
+
dependencies: its only dependency is `@localess/model`, the shared,
|
|
12
|
+
dependency-free domain-model package (ADR 009).
|
|
13
|
+
|
|
14
|
+
`@localess/cli`'s `schema pull`/`push`/`diff`/`validate` commands sync
|
|
15
|
+
definitions written with this package to and from a Localess space. See that
|
|
16
|
+
package's docs for the sync workflow; this package only defines, validates,
|
|
17
|
+
infers, and exports — it does no I/O.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { defineConfig, defineEnum, defineSchema } from '@localess/schema';
|
|
23
|
+
import type { InferContentData } from '@localess/schema';
|
|
24
|
+
|
|
25
|
+
const ButtonType = defineEnum({
|
|
26
|
+
id: 'ButtonType',
|
|
27
|
+
displayName: 'Button Type',
|
|
28
|
+
values: [
|
|
29
|
+
{ name: 'Primary', value: 'primary' },
|
|
30
|
+
{ name: 'Secondary', value: 'secondary' },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const Button = defineSchema({
|
|
35
|
+
id: 'Button',
|
|
36
|
+
type: 'NODE',
|
|
37
|
+
displayName: 'Button',
|
|
38
|
+
previewField: 'label',
|
|
39
|
+
fields: [
|
|
40
|
+
{ name: 'label', kind: 'TEXT', required: true, translatable: true, maxLength: 50 },
|
|
41
|
+
{ name: 'type', kind: 'OPTION', source: ButtonType }, // by-value ref, normalized to 'ButtonType'
|
|
42
|
+
{ name: 'icon', kind: 'ASSET', fileTypes: ['IMAGE'] },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const Page = defineSchema({
|
|
47
|
+
id: 'Page',
|
|
48
|
+
type: 'ROOT',
|
|
49
|
+
fields: [
|
|
50
|
+
{ name: 'title', kind: 'TEXT', required: true },
|
|
51
|
+
{ name: 'blocks', kind: 'SCHEMAS', schemas: [Button] },
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const config = defineConfig({ schemas: [Page, Button, ButtonType] });
|
|
56
|
+
|
|
57
|
+
// Content = { _id: string; _schema: 'Page'; title: string; blocks?: ButtonContent[] }
|
|
58
|
+
export type Content = InferContentData<typeof config>;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API reference
|
|
62
|
+
|
|
63
|
+
| Export | Purpose |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `defineEnum(definition)` | Define an ENUM schema. Identity function; injects `type: 'ENUM'`. |
|
|
66
|
+
| `defineSchema(definition)` | Define a ROOT or NODE schema. Normalizes by-value refs (`source`, `schemas`) to id strings. Throws on duplicate field names. |
|
|
67
|
+
| `defineField(field)` | Define a single field, narrowed by `kind`. Optional; catches a stray property from the wrong kind at the call site, unlike a bare field literal. Identity function. |
|
|
68
|
+
| `defineConfig({ schemas })` | Register the full schema list — the unit the CLI loads and inference resolves against. Throws on duplicate schema ids. |
|
|
69
|
+
| `validate(config)` | Non-throwing `{ ok, issues }` — ID/name patterns, reserved names, length limits, reference resolution. |
|
|
70
|
+
| `toSchemaExport(config)` | Pure mapping to the wire format (`SchemaExport[]`) the Localess API accepts/returns. |
|
|
71
|
+
| `InferContentData<C>` | Union of every ROOT schema's content type in config `C`. |
|
|
72
|
+
| `InferContent<S, C>` | Content type of one schema definition `S`, resolved against config `C`. |
|
|
73
|
+
| `InferEnum<E>` | Literal union of an enum definition's values. |
|
|
74
|
+
|
|
75
|
+
### Exported types
|
|
76
|
+
|
|
77
|
+
Everything below is `export type` — importable with `import type { ... } from '@localess/schema'`.
|
|
78
|
+
|
|
79
|
+
| Group | Types |
|
|
80
|
+
|---|---|
|
|
81
|
+
| Authoring (from `define.ts`) | `EnumDefinition` (result of `defineEnum`), `ComponentDefinition` (result of `defineSchema`), `SchemaDefinition` (`ComponentDefinition \| EnumDefinition`), `LocalessSchemaConfig` (`{ schemas: readonly SchemaDefinition[] }` — what `validate`/`toSchemaExport` accept), `SchemaFieldInput` (a field as authored: `source`/`schemas` accept by-value refs), `EnumDefinitionInput`, `ComponentDefinitionInput` |
|
|
82
|
+
| Inference (from `infer.ts`) | `InferContentData`, `InferContent`, `InferEnum` |
|
|
83
|
+
| Validation (from `validate.ts`) | `ValidationIssue` (`{ severity: 'error' \| 'warning'; code; path; message }`), `ValidationResult` (`{ ok; issues }`) |
|
|
84
|
+
| Wire model (re-exported from `@localess/model`) | `SchemaType`, `SchemaFieldKind`, `AssetFileType`, `SchemaEnumValue`, `SchemaFieldBase`, `SchemaField`, the 18 per-kind interfaces (`SchemaFieldText`, `SchemaFieldTextarea`, `SchemaFieldRichText`, `SchemaFieldMarkdown`, `SchemaFieldNumber`, `SchemaFieldColor`, `SchemaFieldDate`, `SchemaFieldDateTime`, `SchemaFieldBoolean`, `SchemaFieldOption`, `SchemaFieldOptions`, `SchemaFieldLink`, `SchemaFieldReference`, `SchemaFieldReferences`, `SchemaFieldAsset`, `SchemaFieldAssets`, `SchemaFieldSchema`, `SchemaFieldSchemas`), `SchemaComponentExport`, `SchemaEnumExport`, `SchemaExport` |
|
|
85
|
+
| Content values (re-exported from `@localess/model`) | `ContentAsset`, `ContentLink`, `ContentReference`, `ContentRichText` |
|
|
86
|
+
|
|
87
|
+
## `defineField`
|
|
88
|
+
|
|
89
|
+
Optional. Wraps a single field so TypeScript catches a stray property from
|
|
90
|
+
the wrong `kind` at the call site — something a bare field literal inside
|
|
91
|
+
`defineSchema({ fields: [...] })` cannot do:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { defineField } from '@localess/schema';
|
|
95
|
+
|
|
96
|
+
defineField({ name: 'amount', kind: 'NUMBER', maxLength: 5 });
|
|
97
|
+
// ^ compile error: maxLength is not valid on a NUMBER field
|
|
98
|
+
|
|
99
|
+
defineField({ name: 'amount', kind: 'NUMBER', minValue: 0 }); // OK
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`defineSchema`'s `fields` array accepts raw literals and `defineField(...)`
|
|
103
|
+
results interchangeably — by-value ref normalization (`source`, `schemas`)
|
|
104
|
+
still happens exclusively in `defineSchema`, regardless of a field's origin.
|
|
105
|
+
|
|
106
|
+
## By-value references
|
|
107
|
+
|
|
108
|
+
`OPTION`/`OPTIONS` fields accept an enum definition (or a string id) in
|
|
109
|
+
`source`; `SCHEMA`/`SCHEMAS` fields accept schema definitions (or string ids)
|
|
110
|
+
in `schemas`. Both are normalized to string ids at runtime by `defineSchema`,
|
|
111
|
+
matching the wire format exactly — but the *type* keeps the literal id, so
|
|
112
|
+
`InferContent` resolves `OPTION` fields to the referenced enum's literal
|
|
113
|
+
value union instead of plain `string`, and `SCHEMA`/`SCHEMAS` fields to the
|
|
114
|
+
allowed schemas' content types (or every `NODE` schema in the config when the
|
|
115
|
+
`schemas` list is omitted).
|
|
116
|
+
|
|
117
|
+
Fallbacks when a reference can't be resolved against the config:
|
|
118
|
+
|
|
119
|
+
- `OPTION`/`OPTIONS` whose `source` id is not in the config → `string` (or
|
|
120
|
+
`string[]`), mirroring the CLI's `type generate` behavior.
|
|
121
|
+
- `InferEnum` of an enum with no `values` → `string`.
|
|
122
|
+
- Unrestricted `SCHEMA`/`SCHEMAS` (no `schemas` list) in a config with no
|
|
123
|
+
`NODE` schemas → `{ _id: string; _schema: string }` (or an array of it).
|
|
124
|
+
- `InferContentData` of a config with no `ROOT` schema → `never`.
|
|
125
|
+
|
|
126
|
+
## Field kinds
|
|
127
|
+
|
|
128
|
+
Every field carries `name`, `kind`, and the base optional properties
|
|
129
|
+
(`displayName`, `required`, `description`, `defaultValue`, `translatable`).
|
|
130
|
+
`required: true` fields are non-optional keys on the inferred content type;
|
|
131
|
+
everything else is optional.
|
|
132
|
+
|
|
133
|
+
| Kind | Extra properties | Inferred type |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `TEXT`, `TEXTAREA`, `MARKDOWN` | `minLength?`, `maxLength?` | `string` |
|
|
136
|
+
| `RICH_TEXT` | `minLength?`, `maxLength?` | `ContentRichText` |
|
|
137
|
+
| `NUMBER` | `minValue?`, `maxValue?` | `number` |
|
|
138
|
+
| `COLOR`, `DATE`, `DATETIME` | — | `string` |
|
|
139
|
+
| `BOOLEAN` | — | `boolean` |
|
|
140
|
+
| `OPTION` | `source` (required; enum id or `defineEnum` result) | literal union of the referenced enum's values |
|
|
141
|
+
| `OPTIONS` | `source` (required), `minValues?`, `maxValues?` | that union, as an array |
|
|
142
|
+
| `LINK` | — | `ContentLink` |
|
|
143
|
+
| `REFERENCE` / `REFERENCES` | `path?` | `ContentReference` / `ContentReference[]` |
|
|
144
|
+
| `ASSET` / `ASSETS` | `fileTypes?: AssetFileType[]`, `fileType?: AssetFileType` | `ContentAsset` / `ContentAsset[]` |
|
|
145
|
+
| `SCHEMA` / `SCHEMAS` | `schemas?` (allowed ids or `defineSchema` results; unrestricted when absent) | allowed schemas' content type / array of it |
|
|
146
|
+
|
|
147
|
+
`AssetFileType` is `'ANY' | 'IMAGE' | 'VIDEO' | 'TEXT' | 'AUDIO' | 'APPLICATION'`.
|
|
148
|
+
|
|
149
|
+
`ContentAsset`, `ContentLink`, `ContentReference`, and `ContentRichText` are
|
|
150
|
+
re-exported from `@localess/model`, the shared domain-model package (see
|
|
151
|
+
ADR 009).
|
|
152
|
+
|
|
153
|
+
## `validate(config)`
|
|
154
|
+
|
|
155
|
+
Non-throwing. Returns `{ ok, issues }`; `ok` is `false` only when at least
|
|
156
|
+
one `error`-severity issue is present (the `ValidationIssue` type allows
|
|
157
|
+
`'warning'`, but every rule currently implemented reports `error`). Every
|
|
158
|
+
schema in the config is checked, not just the first. Rules and their `code`:
|
|
159
|
+
|
|
160
|
+
| Code | Rule |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `schema/invalid-id` | Schema id must match `/^[a-zA-Z][a-zA-Z0-9]+$/` and be 2-50 characters. |
|
|
163
|
+
| `schema/reserved-id` | Schema id must not be (case-insensitively) `Translations`, `Links`, `ContentMetadata`, `ContentReference`, `ContentRichText`, `ContentLink`, `ContentData`, `ContentAsset`, or `Content`. |
|
|
164
|
+
| `schema/display-name-too-long` | Schema `displayName` ≤ 50 characters. |
|
|
165
|
+
| `schema/description-too-long` | Schema `description` ≤ 250 characters. |
|
|
166
|
+
| `schema/invalid-label` | Each label 2-50 characters, no spaces. |
|
|
167
|
+
| `enum/invalid-value-name` | ENUM value `name` 1-50 characters. |
|
|
168
|
+
| `enum/invalid-value` | ENUM `value` 1-50 characters matching `/^[a-zA-Z]$\|^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z0-9]$/`. |
|
|
169
|
+
| `schema/unknown-preview-field` | `previewField` must name a field of the same schema. |
|
|
170
|
+
| `field/invalid-name` | Field name must match `/^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]$/` (camelCase), be 2-30 characters, and not contain `_i18n_`. |
|
|
171
|
+
| `field/reserved-name` | Field name must not be (case-insensitively) `_id` or `_schema`. |
|
|
172
|
+
| `field/display-name-too-long` | Field `displayName` ≤ 30 characters. |
|
|
173
|
+
| `field/description-too-long` | Field `description` ≤ 250 characters. |
|
|
174
|
+
| `field/default-value-too-long` | Field `defaultValue` ≤ 250 characters. |
|
|
175
|
+
| `field/unresolved-source` | `OPTION`/`OPTIONS` `source` must be the id of a schema in the config. |
|
|
176
|
+
| `field/source-not-enum` | ...and that schema must be an `ENUM`. |
|
|
177
|
+
| `field/unresolved-schema-ref` | Every `SCHEMA`/`SCHEMAS` `schemas` entry must be the id of a schema in the config. |
|
|
178
|
+
| `field/schema-ref-is-enum` | ...and that schema must be `ROOT` or `NODE`, not `ENUM`. |
|
|
179
|
+
|
|
180
|
+
`validate` does not check for stray properties from the wrong field kind —
|
|
181
|
+
use `defineField` for that (below). Duplicate field names and duplicate
|
|
182
|
+
schema ids are not validation issues either: `defineSchema`/`defineConfig`
|
|
183
|
+
throw on them as programming errors.
|
|
184
|
+
|
|
185
|
+
## `toSchemaExport(config)`
|
|
186
|
+
|
|
187
|
+
Pure mapping to `SchemaExport[]`, preserving schema order. Strips
|
|
188
|
+
`undefined`-valued keys from each schema and from the items of its array
|
|
189
|
+
properties (`fields`, `values`), so the result round-trips through JSON
|
|
190
|
+
unchanged. Performs no I/O.
|
|
191
|
+
|
|
192
|
+
## Known limitation (mitigated by `defineField`)
|
|
193
|
+
|
|
194
|
+
TypeScript's excess-property check doesn't apply to object literals inside
|
|
195
|
+
an array passed through a `const`-inferred generic parameter (only to
|
|
196
|
+
literals checked directly against a declared type). A stray property from
|
|
197
|
+
the wrong field kind — e.g. `maxLength` on a `NUMBER` field — inside a bare
|
|
198
|
+
field literal in `defineSchema({ fields: [...] })` is therefore not flagged
|
|
199
|
+
at the call site. Wrap the field in `defineField(...)` instead to get that
|
|
200
|
+
check while keeping full literal-type preservation (see "`defineField`"
|
|
201
|
+
above). Missing required properties (e.g. omitting `source` on `OPTION`) are
|
|
202
|
+
still caught either way. `validate()` and the Localess backend's schema
|
|
203
|
+
validation don't check for this either today.
|
package/dist/define.d.ts
CHANGED
|
@@ -37,9 +37,9 @@ type FieldInputOf<F> = F extends {
|
|
|
37
37
|
* against a declared type), so a stray property from a different kind (e.g. `maxLength` on a
|
|
38
38
|
* `NUMBER` field) inside `defineSchema({ fields: [...] })` will not be flagged at the call site.
|
|
39
39
|
* Missing required properties (e.g. omitting `source` on `OPTION`) are still caught, since that is
|
|
40
|
-
* ordinary structural assignability, not a freshness check.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* ordinary structural assignability, not a freshness check. Sanity documents the same limitation
|
|
41
|
+
* for their unwrapped array fields; the alternative is a per-field wrapper function, which this
|
|
42
|
+
* package deliberately avoids (see ADR 008).
|
|
43
43
|
*/
|
|
44
44
|
export type SchemaFieldInput = FieldInputOf<SchemaField>;
|
|
45
45
|
/** Authoring input for defineEnum: type is injected by the helper. */
|
package/dist/infer.d.ts
CHANGED
|
@@ -9,9 +9,11 @@ type SchemasOf<C> = C extends {
|
|
|
9
9
|
type FindById<C, Id> = Extract<SchemasOf<C>, {
|
|
10
10
|
id: Id;
|
|
11
11
|
}>;
|
|
12
|
-
type NodeIds<C> = [
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
type NodeIds<C> = [
|
|
13
|
+
Extract<SchemasOf<C>, {
|
|
14
|
+
type: 'NODE';
|
|
15
|
+
}>
|
|
16
|
+
] extends [never] ? never : Extract<SchemasOf<C>, {
|
|
15
17
|
type: 'NODE';
|
|
16
18
|
}> extends {
|
|
17
19
|
id: infer Id extends string;
|
|
@@ -91,9 +93,11 @@ export type InferContent<S, C> = S extends {
|
|
|
91
93
|
_id: string;
|
|
92
94
|
_schema: Id;
|
|
93
95
|
} & FieldsObject<S, C>> : never;
|
|
94
|
-
type RootIds<C> = [
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
type RootIds<C> = [
|
|
97
|
+
Extract<SchemasOf<C>, {
|
|
98
|
+
type: 'ROOT';
|
|
99
|
+
}>
|
|
100
|
+
] extends [never] ? never : Extract<SchemasOf<C>, {
|
|
97
101
|
type: 'ROOT';
|
|
98
102
|
}> extends {
|
|
99
103
|
id: infer Id extends string;
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@localess/schema",
|
|
3
|
-
"version": "4.0.0
|
|
4
|
-
"description": "Programmatic schema definitions for Localess with TypeScript content type inference.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"localess",
|
|
7
|
-
"sdk",
|
|
8
|
-
"schema",
|
|
9
|
-
"cms",
|
|
10
|
-
"javascript",
|
|
11
|
-
"typescript"
|
|
12
|
-
],
|
|
13
|
-
"author": "Lessify",
|
|
14
|
-
"homepage": "https://github.com/Lessify/localess-js",
|
|
15
|
-
"sideEffects": false,
|
|
16
|
-
"files": [
|
|
17
|
-
"dist",
|
|
18
|
-
"SKILL.md"
|
|
19
|
-
],
|
|
20
|
-
"main": "dist/index.js",
|
|
21
|
-
"module": "dist/index.mjs",
|
|
22
|
-
"types": "dist/index.d.ts",
|
|
23
|
-
"exports": {
|
|
24
|
-
".": {
|
|
25
|
-
"types": "./dist/index.d.ts",
|
|
26
|
-
"import": "./dist/index.mjs",
|
|
27
|
-
"require": "./dist/index.js"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/Lessify/localess-js.git",
|
|
33
|
-
"directory": "packages/schema"
|
|
34
|
-
},
|
|
35
|
-
"bugs": {
|
|
36
|
-
"url": "https://github.com/Lessify/localess-js/issues"
|
|
37
|
-
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "vite build",
|
|
40
|
-
"test": "vitest run",
|
|
41
|
-
"test:watch": "vitest",
|
|
42
|
-
"test:coverage": "vitest run --coverage"
|
|
43
|
-
},
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"@localess/model": "
|
|
47
|
-
},
|
|
48
|
-
"devDependencies": {
|
|
49
|
-
"@types/node": "^24",
|
|
50
|
-
"typescript": "^5.9.3",
|
|
51
|
-
"vite": "^8.0.16",
|
|
52
|
-
"vite-plugin-dts": "^5.0.0"
|
|
53
|
-
},
|
|
54
|
-
"engines": {
|
|
55
|
-
"node": ">= 24.0.0"
|
|
56
|
-
}
|
|
57
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@localess/schema",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Programmatic schema definitions for Localess with TypeScript content type inference.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"localess",
|
|
7
|
+
"sdk",
|
|
8
|
+
"schema",
|
|
9
|
+
"cms",
|
|
10
|
+
"javascript",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"author": "Lessify",
|
|
14
|
+
"homepage": "https://github.com/Lessify/localess-js",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"SKILL.md"
|
|
19
|
+
],
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"module": "dist/index.mjs",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.mjs",
|
|
27
|
+
"require": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/Lessify/localess-js.git",
|
|
33
|
+
"directory": "packages/schema"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/Lessify/localess-js/issues"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "vite build",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"test:coverage": "vitest run --coverage"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@localess/model": "*"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^24",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vite": "^8.0.16",
|
|
52
|
+
"vite-plugin-dts": "^5.0.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">= 24.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|