@stonecrop/schema 0.8.6 → 0.8.7

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/README.md ADDED
@@ -0,0 +1,329 @@
1
+ # @stonecrop/schema
2
+
3
+ Schema definitions and validation for Stonecrop doctypes, fields, and workflows.
4
+
5
+ ## Overview
6
+
7
+ `@stonecrop/schema` provides the foundational type system for Stonecrop applications. It defines strongly-typed schemas using [Zod](https://zod.dev/) for:
8
+
9
+ - **Field definitions** (`FieldMeta`) - Unified field configuration for forms and tables
10
+ - **Doctype definitions** (`DoctypeMeta`) - Complete document type schemas
11
+ - **Workflows** (`WorkflowMeta`) - State machines and action definitions
12
+ - **Validation** - Runtime schema validation with detailed error reporting
13
+ - **DDL Conversion** - PostgreSQL DDL to Stonecrop schema transformation
14
+
15
+ This package is schema-only and has no UI dependencies - it can be used in both frontend and backend contexts.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # From the monorepo root
21
+ rush update
22
+
23
+ # Or with pnpm
24
+ pnpm add @stonecrop/schema
25
+ ```
26
+
27
+ ## Core Concepts
28
+
29
+ ### Field Types
30
+
31
+ Stonecrop uses semantic field types that remain consistent whether rendered in a form or table:
32
+
33
+ ```typescript
34
+ import { StonecropFieldType, FieldMeta } from '@stonecrop/schema'
35
+
36
+ // Field types include:
37
+ // Text: Data, Text
38
+ // Numeric: Int, Float, Decimal, Currency, Quantity
39
+ // Boolean: Check
40
+ // Date/Time: Date, Time, Datetime, Duration, DateRange
41
+ // Structured: JSON, Code
42
+ // Relational: Link, Doctype
43
+ // Files: Attach
44
+ // Selection: Select
45
+ ```
46
+
47
+ ### Field Metadata
48
+
49
+ `FieldMeta` is the single source of truth for field definitions:
50
+
51
+ ```typescript
52
+ import { FieldMeta } from '@stonecrop/schema'
53
+
54
+ const field: FieldMeta = {
55
+ fieldname: 'customer_name',
56
+ fieldtype: 'Data',
57
+ label: 'Customer Name',
58
+ required: true,
59
+ readOnly: false,
60
+ width: '40ch',
61
+ align: 'left',
62
+ }
63
+
64
+ // Type-specific options
65
+ const linkField: FieldMeta = {
66
+ fieldname: 'customer',
67
+ fieldtype: 'Link',
68
+ label: 'Customer',
69
+ options: 'customer', // Target doctype slug
70
+ }
71
+
72
+ const selectField: FieldMeta = {
73
+ fieldname: 'status',
74
+ fieldtype: 'Select',
75
+ label: 'Status',
76
+ options: ['Draft', 'Submitted', 'Cancelled'], // Choices array
77
+ }
78
+
79
+ const decimalField: FieldMeta = {
80
+ fieldname: 'price',
81
+ fieldtype: 'Decimal',
82
+ label: 'Price',
83
+ options: { precision: 10, scale: 2 }, // Config object
84
+ }
85
+ ```
86
+
87
+ ### Doctype Metadata
88
+
89
+ `DoctypeMeta` defines a complete doctype with fields, workflow, and inheritance:
90
+
91
+ ```typescript
92
+ import { DoctypeMeta } from '@stonecrop/schema'
93
+
94
+ const doctype: DoctypeMeta = {
95
+ name: 'Sales Order',
96
+ slug: 'sales-order',
97
+ tableName: 'sales_order',
98
+ fields: [
99
+ {
100
+ fieldname: 'customer',
101
+ fieldtype: 'Link',
102
+ label: 'Customer',
103
+ options: 'customer',
104
+ required: true,
105
+ },
106
+ {
107
+ fieldname: 'items',
108
+ fieldtype: 'Doctype',
109
+ label: 'Order Items',
110
+ options: 'sales-order-item', // Child doctype
111
+ },
112
+ ],
113
+ workflow: {
114
+ states: ['Draft', 'Submitted', 'Cancelled'],
115
+ actions: {
116
+ submit: {
117
+ label: 'Submit',
118
+ handler: 'submitOrder',
119
+ requiredFields: ['customer', 'items'],
120
+ allowedStates: ['Draft'],
121
+ },
122
+ },
123
+ },
124
+ }
125
+ ```
126
+
127
+ ### Workflow and Actions
128
+
129
+ Define state machines and actions for doctypes:
130
+
131
+ ```typescript
132
+ import { WorkflowMeta, ActionDefinition } from '@stonecrop/schema'
133
+
134
+ const workflow: WorkflowMeta = {
135
+ states: ['Draft', 'Pending Approval', 'Approved', 'Rejected'],
136
+ actions: {
137
+ submit: {
138
+ label: 'Submit for Approval',
139
+ handler: 'handleSubmit',
140
+ requiredFields: ['title', 'description'],
141
+ allowedStates: ['Draft'],
142
+ confirm: true,
143
+ },
144
+ approve: {
145
+ label: 'Approve',
146
+ handler: 'handleApprove',
147
+ allowedStates: ['Pending Approval'],
148
+ args: { notifyUser: true },
149
+ },
150
+ },
151
+ }
152
+ ```
153
+
154
+ ## Validation
155
+
156
+ Runtime validation with detailed error reporting:
157
+
158
+ ```typescript
159
+ import { validateField, validateDoctype } from '@stonecrop/schema'
160
+
161
+ // Validate a field definition
162
+ const fieldResult = validateField({
163
+ fieldname: 'email',
164
+ fieldtype: 'Data',
165
+ label: 'Email',
166
+ })
167
+
168
+ if (!fieldResult.success) {
169
+ console.error('Validation errors:', fieldResult.errors)
170
+ // errors: [{ path: ['fieldname'], message: 'Required' }]
171
+ }
172
+
173
+ // Validate a doctype definition
174
+ const doctypeResult = validateDoctype(doctypeData)
175
+
176
+ if (doctypeResult.success) {
177
+ console.log('Doctype is valid!')
178
+ }
179
+ ```
180
+
181
+ ### Parse and Validate
182
+
183
+ Use Zod's parse methods for type-safe validation:
184
+
185
+ ```typescript
186
+ import { parseField, parseDoctype } from '@stonecrop/schema'
187
+
188
+ try {
189
+ const field = parseField(untrustedData)
190
+ // TypeScript knows field is FieldMeta
191
+ } catch (error) {
192
+ console.error('Invalid field:', error)
193
+ }
194
+
195
+ try {
196
+ const doctype = parseDoctype(untrustedData)
197
+ // TypeScript knows doctype is DoctypeMeta
198
+ } catch (error) {
199
+ console.error('Invalid doctype:', error)
200
+ }
201
+ ```
202
+
203
+ ## DDL Conversion
204
+
205
+ Convert PostgreSQL DDL statements to Stonecrop doctype schemas:
206
+
207
+ ```typescript
208
+ import { convertSchema, type ConversionOptions } from '@stonecrop/schema'
209
+
210
+ const ddl = `
211
+ CREATE TABLE customers (
212
+ id SERIAL PRIMARY KEY,
213
+ name VARCHAR(255) NOT NULL,
214
+ email VARCHAR(255) UNIQUE,
215
+ created_at TIMESTAMP DEFAULT NOW()
216
+ );
217
+
218
+ CREATE TABLE sales_orders (
219
+ id SERIAL PRIMARY KEY,
220
+ customer_id INTEGER REFERENCES customers(id),
221
+ status VARCHAR(20) DEFAULT 'Draft',
222
+ total_amount DECIMAL(10, 2)
223
+ );
224
+ `
225
+
226
+ const options: ConversionOptions = {
227
+ inheritanceMode: 'flatten', // or 'reference'
228
+ useCamelCase: true, // Convert snake_case to camelCase
229
+ includeUnmappedMeta: false, // Include unmapped metadata
230
+ schema: 'public', // Filter by schema
231
+ exclude: ['migrations'], // Exclude tables
232
+ typeOverrides: {
233
+ status: { fieldtype: 'Select', options: ['Draft', 'Submitted'] },
234
+ },
235
+ }
236
+
237
+ const doctypes = convertSchema(ddl, options)
238
+
239
+ doctypes.forEach(doctype => {
240
+ console.log(`Doctype: ${doctype.name}`)
241
+ console.log(`Table: ${doctype.tableName}`)
242
+ console.log(`Fields: ${doctype.fields.length}`)
243
+ })
244
+ ```
245
+
246
+ ### Naming Utilities
247
+
248
+ Convert between different naming conventions:
249
+
250
+ ```typescript
251
+ import {
252
+ snakeToCamel,
253
+ camelToSnake,
254
+ snakeToLabel,
255
+ camelToLabel,
256
+ toPascalCase,
257
+ toSlug,
258
+ } from '@stonecrop/schema'
259
+
260
+ snakeToCamel('customer_name') // 'customerName'
261
+ camelToSnake('customerName') // 'customer_name'
262
+ snakeToLabel('customer_name') // 'Customer Name'
263
+ camelToLabel('customerName') // 'Customer Name'
264
+ toPascalCase('customer_name') // 'CustomerName'
265
+ toSlug('Customer Name') // 'customer-name'
266
+ ```
267
+
268
+ ## API
269
+
270
+ ### Field Type Mapping
271
+
272
+ ```typescript
273
+ import { TYPE_MAP, getDefaultComponent } from '@stonecrop/schema'
274
+
275
+ // Get default component for a field type
276
+ const component = getDefaultComponent('Data') // 'ATextInput'
277
+
278
+ // Access full type map
279
+ console.log(TYPE_MAP['Link']) // { component: 'ALink', fieldtype: 'Link' }
280
+ ```
281
+
282
+ ## Usage in Stonecrop
283
+
284
+ This package provides the type system used throughout Stonecrop:
285
+
286
+ - **`@stonecrop/stonecrop`** - Registry uses `DoctypeMeta` for schema storage
287
+ - **`@stonecrop/aform`** - Renders fields based on `FieldMeta` definitions
288
+ - **`@stonecrop/atable`** - Uses `FieldMeta` for column configuration
289
+ - **Backend APIs** - Validates and stores doctypes using these schemas
290
+
291
+ ## Development
292
+
293
+ ```bash
294
+ # Install dependencies
295
+ rush update
296
+
297
+ # Build
298
+ rushx build
299
+
300
+ # Run tests
301
+ rushx test
302
+
303
+ # Watch mode
304
+ rushx test:watch
305
+
306
+ # Generate API documentation
307
+ rushx docs
308
+ ```
309
+
310
+ ## TypeScript Support
311
+
312
+ This package is written in TypeScript with strict mode enabled and provides full type definitions:
313
+
314
+ ```typescript
315
+ import type { FieldMeta, DoctypeMeta } from '@stonecrop/schema'
316
+
317
+ // Types are inferred from Zod schemas
318
+ const field: FieldMeta = {
319
+ fieldname: 'title',
320
+ fieldtype: 'Data',
321
+ // TypeScript will catch typos and missing required fields
322
+ }
323
+
324
+ // Use Zod's infer utility for derived types
325
+ import { z } from 'zod'
326
+ import { FieldMeta as FieldMetaSchema } from '@stonecrop/schema'
327
+
328
+ type FieldMetaType = z.infer<typeof FieldMetaSchema>
329
+ ```
package/dist/cli.cjs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ "use strict";const r=require("node:fs"),c=require("node:path"),w=require("node:util"),x=require("graphql"),v=require("./index-aeXXzPET.cjs");async function O(e,h){const t=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",...h},body:JSON.stringify({query:x.getIntrospectionQuery()})});if(!t.ok)throw new Error(`Failed to fetch introspection: ${t.status} ${t.statusText}`);const a=await t.json();if(a.errors?.length)throw new Error(`GraphQL errors: ${a.errors.map(n=>n.message).join(", ")}`);if(!a.data)throw new Error("No data in introspection response");return a.data}async function $(){const{values:e,positionals:h}=w.parseArgs({allowPositionals:!0,options:{endpoint:{type:"string",short:"e"},introspection:{type:"string",short:"i"},sdl:{type:"string",short:"s"},output:{type:"string",short:"o"},include:{type:"string"},exclude:{type:"string"},overrides:{type:"string"},"custom-scalars":{type:"string"},"include-unmapped":{type:"boolean",default:!1},help:{type:"boolean",short:"h"}}}),t=h[0];(e.help||!t)&&(q(),process.exit(t?0:1)),t!=="generate"&&(console.error(`Unknown command: ${t}`),console.error("Available commands: generate"),process.exit(1)),[e.endpoint,e.introspection,e.sdl].filter(Boolean).length!==1&&(console.error("Exactly one of --endpoint, --introspection, or --sdl must be provided"),process.exit(1)),e.output||(console.error("--output <dir> is required"),process.exit(1));const n=c.resolve(e.output),l={includeUnmappedMeta:e["include-unmapped"]};if(e.include&&(l.include=e.include.split(",").map(o=>o.trim())),e.exclude&&(l.exclude=e.exclude.split(",").map(o=>o.trim())),e.overrides){const o=c.resolve(e.overrides),s=r.readFileSync(o,"utf-8");l.typeOverrides=JSON.parse(s)}if(e["custom-scalars"]){const o=c.resolve(e["custom-scalars"]),s=r.readFileSync(o,"utf-8");l.customScalars=JSON.parse(s)}let p;if(e.endpoint)console.log(`Fetching introspection from ${e.endpoint}...`),p=await O(e.endpoint);else if(e.introspection){const o=c.resolve(e.introspection),s=r.readFileSync(o,"utf-8"),u=JSON.parse(s);p=u.data??u}else{const o=c.resolve(e.sdl);p=r.readFileSync(o,"utf-8")}const m=v.convertGraphQLSchema(p,l);m.length===0&&(console.warn("No entity types found in the schema. Check your include/exclude filters."),process.exit(0)),r.existsSync(n)||r.mkdirSync(n,{recursive:!0});let f=0,d=0;for(const o of m){const s=`${o.slug}.json`,u=c.join(n,s),S=JSON.stringify(o,null," ");r.writeFileSync(u,S+`
3
+ `,"utf-8");const g=v.validateDoctype(o);if(g.success){const i=o.fields.filter(y=>y._unmapped);i.length>0&&(f++,console.warn(` WARN: ${s} has ${i.length} unmapped field(s): ${i.map(y=>y.fieldname).join(", ")}`))}else{d++,console.error(` ERROR: ${s} failed validation:`);for(const i of g.errors)console.error(` ${i.path.join(".")}: ${i.message}`)}}console.log(`
4
+ Generated ${m.length} doctype(s) in ${n}`+(f?` (${f} with warnings)`:"")+(d?` (${d} with errors)`:"")),d>0&&process.exit(1)}function q(){console.log(`
5
+ stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
6
+
7
+ USAGE:
8
+ stonecrop-schema generate [options]
9
+
10
+ SOURCE (exactly one required):
11
+ --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint
12
+ --introspection, -i <file> Read from a saved introspection JSON file
13
+ --sdl, -s <file> Read from a GraphQL SDL (.graphql) file
14
+
15
+ OUTPUT:
16
+ --output, -o <dir> Directory to write doctype JSON files (required)
17
+
18
+ OPTIONS:
19
+ --include <types> Comma-separated list of type names to include
20
+ --exclude <types> Comma-separated list of type names to exclude
21
+ --overrides <file> JSON file with per-type field overrides
22
+ --custom-scalars <file> JSON file mapping custom scalar names to field templates
23
+ --include-unmapped Include _graphqlType metadata on unmapped fields
24
+ --help, -h Show this help message
25
+
26
+ EXAMPLES:
27
+ # From a live PostGraphile server
28
+ stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas
29
+
30
+ # From a saved introspection result
31
+ stonecrop-schema generate -i introspection.json -o ./schemas
32
+
33
+ # From an SDL file with custom scalars
34
+ stonecrop-schema generate -s schema.graphql -o ./schemas \\
35
+ --custom-scalars custom-scalars.json
36
+
37
+ # Only convert specific types
38
+ stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\
39
+ --include "User,Post,Comment"
40
+ `)}$().catch(e=>{console.error("Error:",e.message),process.exit(1)});
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync as u, existsSync as S, mkdirSync as w, writeFileSync as x } from "node:fs";
3
+ import { resolve as c, join as O } from "node:path";
4
+ import { parseArgs as $ } from "node:util";
5
+ import { getIntrospectionQuery as N } from "graphql";
6
+ import { h as P, v as j } from "./index-COrltkHl.js";
7
+ async function C(e, m) {
8
+ const t = await fetch(e, {
9
+ method: "POST",
10
+ headers: {
11
+ "Content-Type": "application/json",
12
+ ...m
13
+ },
14
+ body: JSON.stringify({
15
+ query: N()
16
+ })
17
+ });
18
+ if (!t.ok)
19
+ throw new Error(`Failed to fetch introspection: ${t.status} ${t.statusText}`);
20
+ const i = await t.json();
21
+ if (i.errors?.length)
22
+ throw new Error(`GraphQL errors: ${i.errors.map((r) => r.message).join(", ")}`);
23
+ if (!i.data)
24
+ throw new Error("No data in introspection response");
25
+ return i.data;
26
+ }
27
+ async function E() {
28
+ const { values: e, positionals: m } = $({
29
+ allowPositionals: !0,
30
+ options: {
31
+ endpoint: { type: "string", short: "e" },
32
+ introspection: { type: "string", short: "i" },
33
+ sdl: { type: "string", short: "s" },
34
+ output: { type: "string", short: "o" },
35
+ include: { type: "string" },
36
+ exclude: { type: "string" },
37
+ overrides: { type: "string" },
38
+ "custom-scalars": { type: "string" },
39
+ "include-unmapped": { type: "boolean", default: !1 },
40
+ help: { type: "boolean", short: "h" }
41
+ }
42
+ }), t = m[0];
43
+ (e.help || !t) && (q(), process.exit(t ? 0 : 1)), t !== "generate" && (console.error(`Unknown command: ${t}`), console.error("Available commands: generate"), process.exit(1)), [e.endpoint, e.introspection, e.sdl].filter(Boolean).length !== 1 && (console.error("Exactly one of --endpoint, --introspection, or --sdl must be provided"), process.exit(1)), e.output || (console.error("--output <dir> is required"), process.exit(1));
44
+ const r = c(e.output), a = {
45
+ includeUnmappedMeta: e["include-unmapped"]
46
+ };
47
+ if (e.include && (a.include = e.include.split(",").map((o) => o.trim())), e.exclude && (a.exclude = e.exclude.split(",").map((o) => o.trim())), e.overrides) {
48
+ const o = c(e.overrides), s = u(o, "utf-8");
49
+ a.typeOverrides = JSON.parse(s);
50
+ }
51
+ if (e["custom-scalars"]) {
52
+ const o = c(e["custom-scalars"]), s = u(o, "utf-8");
53
+ a.customScalars = JSON.parse(s);
54
+ }
55
+ let l;
56
+ if (e.endpoint)
57
+ console.log(`Fetching introspection from ${e.endpoint}...`), l = await C(e.endpoint);
58
+ else if (e.introspection) {
59
+ const o = c(e.introspection), s = u(o, "utf-8"), d = JSON.parse(s);
60
+ l = d.data ?? d;
61
+ } else {
62
+ const o = c(e.sdl);
63
+ l = u(o, "utf-8");
64
+ }
65
+ const f = P(l, a);
66
+ f.length === 0 && (console.warn("No entity types found in the schema. Check your include/exclude filters."), process.exit(0)), S(r) || w(r, { recursive: !0 });
67
+ let h = 0, p = 0;
68
+ for (const o of f) {
69
+ const s = `${o.slug}.json`, d = O(r, s), v = JSON.stringify(o, null, " ");
70
+ x(d, v + `
71
+ `, "utf-8");
72
+ const g = j(o);
73
+ if (g.success) {
74
+ const n = o.fields.filter((y) => y._unmapped);
75
+ n.length > 0 && (h++, console.warn(
76
+ ` WARN: ${s} has ${n.length} unmapped field(s): ${n.map((y) => y.fieldname).join(", ")}`
77
+ ));
78
+ } else {
79
+ p++, console.error(` ERROR: ${s} failed validation:`);
80
+ for (const n of g.errors)
81
+ console.error(` ${n.path.join(".")}: ${n.message}`);
82
+ }
83
+ }
84
+ console.log(
85
+ `
86
+ Generated ${f.length} doctype(s) in ${r}` + (h ? ` (${h} with warnings)` : "") + (p ? ` (${p} with errors)` : "")
87
+ ), p > 0 && process.exit(1);
88
+ }
89
+ function q() {
90
+ console.log(`
91
+ stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
92
+
93
+ USAGE:
94
+ stonecrop-schema generate [options]
95
+
96
+ SOURCE (exactly one required):
97
+ --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint
98
+ --introspection, -i <file> Read from a saved introspection JSON file
99
+ --sdl, -s <file> Read from a GraphQL SDL (.graphql) file
100
+
101
+ OUTPUT:
102
+ --output, -o <dir> Directory to write doctype JSON files (required)
103
+
104
+ OPTIONS:
105
+ --include <types> Comma-separated list of type names to include
106
+ --exclude <types> Comma-separated list of type names to exclude
107
+ --overrides <file> JSON file with per-type field overrides
108
+ --custom-scalars <file> JSON file mapping custom scalar names to field templates
109
+ --include-unmapped Include _graphqlType metadata on unmapped fields
110
+ --help, -h Show this help message
111
+
112
+ EXAMPLES:
113
+ # From a live PostGraphile server
114
+ stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas
115
+
116
+ # From a saved introspection result
117
+ stonecrop-schema generate -i introspection.json -o ./schemas
118
+
119
+ # From an SDL file with custom scalars
120
+ stonecrop-schema generate -s schema.graphql -o ./schemas \\
121
+ --custom-scalars custom-scalars.json
122
+
123
+ # Only convert specific types
124
+ stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\
125
+ --include "User,Post,Comment"
126
+ `);
127
+ }
128
+ E().catch((e) => {
129
+ console.error("Error:", e.message), process.exit(1);
130
+ });