@pylonts/dsl 1.1.16 → 1.1.17

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.
@@ -1,5 +1,7 @@
1
1
  import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef, isDtoField, isDtoMessage } from './dto.js';
2
2
  import { collectEnumRefs, EnumField, Field } from './dsl.js';
3
+ import type { TableSchema } from './db.js';
4
+ import type { TokenSchema } from './token.js';
3
5
 
4
6
  // TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
5
7
  // Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
@@ -32,6 +34,21 @@ function dtoFieldDescription(f: DtoField): string | undefined {
32
34
  return f.description ?? fieldDescription(f.field);
33
35
  }
34
36
 
37
+ /** Resolve a ref chain to its terminal field (the one without .ref).
38
+ * Cycles are a DSL definition error — fail loudly at render time. */
39
+ function resolveRefChain(f: DtoField): DtoField {
40
+ const seen = new Set<DtoField>();
41
+ let cur: DtoField = f;
42
+ while (cur.ref !== undefined) {
43
+ if (seen.has(cur.ref)) {
44
+ throw new Error(`dto field ${cur.name}: circular ref chain (field references itself)`);
45
+ }
46
+ seen.add(cur.ref);
47
+ cur = cur.ref;
48
+ }
49
+ return cur;
50
+ }
51
+
35
52
  function renderBasic(
36
53
  field: Field,
37
54
  pattern: string | undefined,
@@ -171,11 +188,36 @@ function renderObject(
171
188
  }
172
189
 
173
190
  function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
191
+ // Ref branch renders its own optional (referencing overrides + chain fallback).
192
+ if (f.ref !== undefined) return renderValue(f, indent, resolver);
174
193
  const base = renderValue(f, indent, resolver);
175
194
  return f.isOptional() ? `Type.Optional(${base})` : base;
176
195
  }
177
196
 
197
+ /** Render a ref-carrying field's bare type (no optional wrapper): resolve the
198
+ * chain, inherit the terminal field's type/constraints, keep the referencing
199
+ * field's own overrides (pattern / default / description). */
200
+ function renderRefBase(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
201
+ const target = resolveRefChain(f);
202
+ const targetField = target.field as Field;
203
+ const pattern = f.pattern ?? target.pattern;
204
+ const defaultValue = f.default ?? target.default;
205
+ const desc = f.description ?? dtoFieldDescription(target);
206
+ return renderBasic(targetField, pattern, defaultValue, resolver, indent, desc);
207
+ }
208
+
209
+ /** Render a ref-carrying field with its optional wrapper: referencing
210
+ * override first, then the chain's DtoField-level optional, then the bare
211
+ * column optionality. */
212
+ function renderRefField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
213
+ const target = resolveRefChain(f);
214
+ const optional = f.optional ?? target.optional ?? (target.field as Field).optional ?? false;
215
+ const base = renderRefBase(f, indent, resolver);
216
+ return optional ? `Type.Optional(${base})` : base;
217
+ }
218
+
178
219
  function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
220
+ if (f.ref !== undefined) return renderRefField(f, indent, resolver);
179
221
  if (f.field.type === 'array') {
180
222
  const items = f.field.items;
181
223
  const desc = dtoFieldDescription(f);
@@ -210,6 +252,10 @@ function collectEnumImports(
210
252
  resolver: EnumResolver | undefined,
211
253
  out: Map<string, ImportBase>,
212
254
  ): void {
255
+ if (f.ref !== undefined) {
256
+ collectEnumImports(resolveRefChain(f), resolver, out);
257
+ return;
258
+ }
213
259
  if (f.field.type === 'array') {
214
260
  const items = f.field.items;
215
261
  if (isDtoMessage(items)) return;
@@ -257,14 +303,41 @@ export function collectDtoImports(
257
303
  for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, out);
258
304
  }
259
305
 
306
+ /** Render the server-injection base: token-injected fields as Optional
307
+ * properties of a TypeBox object, plus a non-enumerable __inject adapter
308
+ * (same mechanism as hand-written bases, see pylon __inject docs) that fills
309
+ * each field from the token at runtime. */
310
+ function renderInjectBase(fields: Record<string, DtoField>, resolver: EnumResolver | undefined): string {
311
+ const entries = Object.entries(fields).map(([name, f]) => {
312
+ const base = f.ref !== undefined ? renderRefBase(f, 1, resolver) : renderValue(f, 1, resolver);
313
+ return ` ${name}: Type.Optional(${base})`;
314
+ });
315
+ const inner = `Type.Object({\n${entries.join(',\n')}\n})`;
316
+ const assigns = Object.keys(fields)
317
+ .map((k) => `body.${k} = token.${k};`)
318
+ .join(' ');
319
+ return [
320
+ `Object.defineProperty(`,
321
+ ` ${inner},`,
322
+ ` '__inject',`,
323
+ ` { value: (body: Record<string, unknown>, token: Record<string, unknown>): void => { ${assigns} }, enumerable: false },`,
324
+ `)`,
325
+ ].join('\n');
326
+ }
327
+
260
328
  /** Render one DTO export (const + type) — no file header, for file-level generation. */
261
329
  export function renderDtoExport(schema: DtoMessage, resolver: EnumResolver | undefined): string {
262
- const object = renderObject(schema.fields, 1, resolver, schema.description);
263
- const bases = schema.bases ?? [];
264
- const body =
265
- bases.length > 0
266
- ? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
267
- : object;
330
+ const injectFields: Record<string, DtoField> = {};
331
+ const normalFields: Record<string, DtoField> = {};
332
+ for (const [key, f] of Object.entries(schema.fields)) {
333
+ if (f.injectFrom !== undefined) injectFields[key] = f;
334
+ else normalFields[key] = f;
335
+ }
336
+ const parts: string[] = [];
337
+ if (Object.keys(injectFields).length > 0) parts.push(renderInjectBase(injectFields, resolver));
338
+ parts.push(renderObject(normalFields, 1, resolver, schema.description));
339
+ for (const base of schema.bases ?? []) parts.push(renderBase(base));
340
+ const body = parts.length > 1 ? `Type.Intersect([${parts.join(', ')}])` : parts[0];
268
341
  return `export const ${schema.name} = ${body};`;
269
342
  }
270
343
 
@@ -273,6 +346,55 @@ export function renderDtoTypeExport(name: string): string {
273
346
  return `export type ${name} = Static<typeof ${name}>;`;
274
347
  }
275
348
 
349
+ /** Render one token export: a flat TypeBox object. Security fields keep
350
+ * their declared optionality (secret required, cipher optional); identity
351
+ * fields are Optional except the primary-key anchor — they only exist after
352
+ * login (two-state object, flat runtime shape), while the PK is guaranteed
353
+ * to be projected (token validation enforces it) and serves as the identity
354
+ * anchor (e.g. the tenant key for tenant-scoped controllers). */
355
+ export function renderTokenExport(token: TokenSchema, resolver: EnumResolver | undefined): string {
356
+ const constName = `${token.name}Token`;
357
+ const entries = [
358
+ ...Object.entries(token.security).map(([name, f]) => ` ${name}: ${renderField(f, 1, resolver)}`),
359
+ ...Object.entries(token.identity).map(([name, f]) =>
360
+ isPrimaryKeyProjection(f) ? ` ${name}: ${renderValue(f, 1, resolver)}` : ` ${name}: Type.Optional(${renderValue(f, 1, resolver)})`,
361
+ ),
362
+ ];
363
+ const desc = token.description !== undefined ? `, { description: ${renderString(token.description)} }` : '';
364
+ return `export const ${constName} = Type.Object({\n${entries.join(',\n')}\n}${desc});`;
365
+ }
366
+
367
+ /** True when the DTO field projects a primary-key column of its source table. */
368
+ function isPrimaryKeyProjection(f: DtoField): boolean {
369
+ const tbl = f.field.schema;
370
+ if (tbl?.type !== 'table') return false;
371
+ // A table-backed field is always a plain Field (only Field carries a table
372
+ // schema); array/object DTO fields reference messages, not tables.
373
+ const field = f.field as Field;
374
+ const table = tbl as TableSchema;
375
+ const pk = table.primaryKey;
376
+ if (!pk) return false;
377
+ const pks = Array.isArray(pk) ? pk : [pk];
378
+ return pks.includes(field);
379
+ }
380
+
381
+ /** Render the Static type export for a token. */
382
+ export function renderTokenTypeExport(name: string): string {
383
+ return `export type ${name}Token = Static<typeof ${name}Token>;`;
384
+ }
385
+
386
+ /** Collect all imports needed to render a token: enum references across
387
+ * security + identity fields. */
388
+ export function collectTokenImports(
389
+ token: TokenSchema,
390
+ resolver: EnumResolver | undefined,
391
+ out: Map<string, ImportBase>,
392
+ ): void {
393
+ for (const f of [...Object.values(token.security), ...Object.values(token.identity)]) {
394
+ collectEnumImports(f, resolver, out);
395
+ }
396
+ }
397
+
276
398
  export function renderDtoMessage(
277
399
  schema: DtoMessage,
278
400
  options: { resolver?: EnumResolver; source?: string } = {},