@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.
package/dist/convert.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import type { CollectionSchemaBase, SchemaBase } from './dsl.js';
2
2
  import type { DtoMessage } from './dto.js';
3
- import type { TableSchema } from './db.js';
4
3
  import type { EntitySchema } from './entity.js';
5
4
  import type { FrontAppSchema, ProjectApiSchema } from './project.js';
6
- /** A source/target collection of a convert — dto, entity or table. Entity
7
- * sources may carry aggregate fields (aggField), e.g. an aggregate result
8
- * entity projected into a wire message. */
9
- export type ConvertSourceSchema = DtoMessage | TableSchema | EntitySchema;
5
+ /** A source/target collection of a convert — dto or entity. Entity sources
6
+ * may carry aggregate fields (aggField), e.g. an aggregate result entity
7
+ * projected into a wire message. */
8
+ export type ConvertSourceSchema = DtoMessage | EntitySchema;
10
9
  /** Method input for defineConvert: type/schema/name are set by the builder. */
11
10
  export type ConvertMethodDef = Omit<ConvertMethodSchema, 'type' | 'schema' | 'name'>;
12
11
  /** One conversion: multiple source collections → single target collection. */
package/dist/dto.d.ts CHANGED
@@ -2,6 +2,7 @@ import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './
2
2
  import { TableSchema } from './db.js';
3
3
  import type { EntitySchema } from './entity.js';
4
4
  import type { ImportBase } from './import-base.js';
5
+ import type { TokenSchema } from './token.js';
5
6
  /** Re-export — Operator lives on the DSL level (see dsl.ts). */
6
7
  export type { Operator } from './dsl.js';
7
8
  /** Re-export — ImportBase lives on its own module (see import-base.ts). */
@@ -49,6 +50,10 @@ export declare class DtoField implements SchemaBase {
49
50
  default?: unknown;
50
51
  /** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
51
52
  ref?: DtoField;
53
+ /** Server-injection marker: this field is filled from the token at runtime
54
+ * (client never sends it). Set by fromToken(); the driver renders it as an
55
+ * Optional field inside a __inject base of the DTO. */
56
+ injectFrom?: TokenSchema;
52
57
  constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef);
53
58
  setPattern(value: string): this;
54
59
  setDescription(value: string): this;
@@ -116,3 +121,11 @@ export type DtoFieldSource = TableSchema | DtoMessage | EntitySchema;
116
121
  * entity) and wrap them as DTO fields (aligned with dto.from). Shared Field
117
122
  * instances keep their original identity — the projection references them. */
118
123
  export declare function from(source: DtoFieldSource, fields: (Field | DtoArrayFieldDef | DtoObjectFieldDef)[]): Record<string, DtoField>;
124
+ /** Project fields from a TokenSchema (security/identity segments) as
125
+ * server-injected DTO fields. Unlike from(), the projection does NOT share
126
+ * the token's DtoField instance — each field is a NEW DtoField wrapping the
127
+ * same underlying Field, referencing the token field via setRef() so the DTO
128
+ * write-back (buildMessage) never mutates the token's own fields. Every
129
+ * produced field is marked injectFrom (rendered inside a __inject base:
130
+ * Optional in the wire schema, filled from the token at runtime). */
131
+ export declare function fromToken(token: TokenSchema, fields: DtoField[]): Record<string, DtoField>;
package/dist/dto.js CHANGED
@@ -16,6 +16,10 @@ export class DtoField {
16
16
  default;
17
17
  /** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
18
18
  ref;
19
+ /** Server-injection marker: this field is filled from the token at runtime
20
+ * (client never sends it). Set by fromToken(); the driver renders it as an
21
+ * Optional field inside a __inject base of the DTO. */
22
+ injectFrom;
19
23
  constructor(field) {
20
24
  this.name = '';
21
25
  this.field = field;
@@ -251,3 +255,27 @@ export function from(source, fields) {
251
255
  }
252
256
  return out;
253
257
  }
258
+ function ownsTokenField(token, field) {
259
+ return (Object.values(token.security).some((df) => df === field) ||
260
+ Object.values(token.identity).some((df) => df === field));
261
+ }
262
+ /** Project fields from a TokenSchema (security/identity segments) as
263
+ * server-injected DTO fields. Unlike from(), the projection does NOT share
264
+ * the token's DtoField instance — each field is a NEW DtoField wrapping the
265
+ * same underlying Field, referencing the token field via setRef() so the DTO
266
+ * write-back (buildMessage) never mutates the token's own fields. Every
267
+ * produced field is marked injectFrom (rendered inside a __inject base:
268
+ * Optional in the wire schema, filled from the token at runtime). */
269
+ export function fromToken(token, fields) {
270
+ const out = {};
271
+ for (const field of fields) {
272
+ if (!ownsTokenField(token, field)) {
273
+ throw new Error(`dto.fromToken(${token.name}): field ${field.name} does not belong to this token`);
274
+ }
275
+ const df = dtoField(field.field);
276
+ df.setRef(field);
277
+ df.injectFrom = token;
278
+ out[field.name] = df;
279
+ }
280
+ return out;
281
+ }
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ export * from './aggregate.js';
31
31
  export * from './repository.js';
32
32
  export * from './domain-event.js';
33
33
  export * from './third-service.js';
34
+ export * from './token.js';
34
35
  export * from './field-rule.js';
35
36
  export * from './exception.js';
36
37
  export * from './page.js';
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ export * from './aggregate.js';
31
31
  export * from './repository.js';
32
32
  export * from './domain-event.js';
33
33
  export * from './third-service.js';
34
+ export * from './token.js';
34
35
  export * from './field-rule.js';
35
36
  export * from './exception.js';
36
37
  export * from './page.js';
@@ -45,9 +45,9 @@ function columnType(field) {
45
45
  function renderDefault(field) {
46
46
  if (field.default === undefined)
47
47
  return '';
48
- // MySQL keyword expressions (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
49
- // NULL, etc.) render bare, no quotes.
50
- if (/^[A-Z]/.test(field.default))
48
+ // MySQL keyword expressions (CURRENT_TIMESTAMP / CURRENT_TIMESTAMP ON UPDATE
49
+ // CURRENT_TIMESTAMP) render bare; any other value is a literal and gets quoted.
50
+ if (field.default.startsWith(CURRENT_TIMESTAMP))
51
51
  return ` DEFAULT ${field.default}`;
52
52
  // Numeric columns take a bare literal, not a quoted one.
53
53
  if (field.type === 'integer' || field.type === 'bigint' || field.type === 'decimal' || field.type === 'rate' || field.type === 'boolean') {
package/dist/service.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { exceptionEndNames } from './flow.js';
2
2
  export function defineService(options) {
3
+ if (!/(Service|Handler)$/.test(options.name)) {
4
+ throw new Error(`service ${options.name}: name must end with 'Service' (or 'Handler' for handler-style services)`);
5
+ }
3
6
  if (options.app && !options.api.apps.includes(options.app)) {
4
7
  throw new Error(`service ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
5
8
  }
@@ -1,17 +1,30 @@
1
1
  import type { SchemaBase } from './dsl.js';
2
2
  import type { DtoMessage } from './dto.js';
3
3
  import type { ExceptionSchema } from './exception.js';
4
- import type { ThirdApiSchema } from './project.js';
4
+ import type { ProjectApiSchema, ThirdApiSchema } from './project.js';
5
5
  /** A third-party integration service (e.g. tenpay wechat pay).
6
6
  * Distinct from ServiceSchema (backend service bound to an app) and
7
7
  * ThirdApiSchema (project topology: the dir the integration lives in).
8
- * Describes the adapter class contract: constructor config + methods. */
8
+ * Describes the adapter class contract: constructor config + methods.
9
+ * `callbacks` are the inbound half — push messages the third party sends to
10
+ * this platform (e.g. audit-result notify), rendered as @Public controllers. */
11
+ /** Wire data format of the third-party gateway. 'form' (urlencoded) and
12
+ * 'json' are built-in in @pylonts/sandbox; any other string is a custom
13
+ * format the sandbox encoder parses itself. The client and the sandbox are
14
+ * mirror images of one protocol — the format declared here is the single
15
+ * source both generated sides read. */
16
+ export type ThirdServiceFormat = 'form' | 'json' | string;
9
17
  export interface ThirdServiceSchema extends SchemaBase {
10
18
  type: 'thirdService';
11
19
  /** The third-party system this service integrates (topology reference). */
12
20
  schema: ThirdApiSchema;
21
+ /** Wire data format of the gateway (see ThirdServiceFormat). Default 'json'. */
22
+ format?: ThirdServiceFormat;
13
23
  /** Methods keyed by name — the map key is written back as the method name. */
14
24
  methods: Record<string, ThirdServiceMethodSchema>;
25
+ /** Callbacks (third-party push → platform) keyed by name. Empty when the
26
+ * integration is outbound-only. */
27
+ callbacks: Record<string, ThirdCallbackSchema>;
15
28
  }
16
29
  /** A method of a third-party integration service. Same contract shape as
17
30
  * ServiceMethodSchema minus flow — the implementation lives in the external
@@ -20,6 +33,11 @@ export interface ThirdServiceSchema extends SchemaBase {
20
33
  export interface ThirdServiceMethodSchema extends SchemaBase {
21
34
  type: 'method';
22
35
  schema: ThirdServiceSchema;
36
+ /** Wire service name sent to the third-party gateway. Defaults to the method
37
+ * key — declare explicitly when the wire value differs (e.g. camelCase key
38
+ * vs snake_case wire: uploadImage → 'pic_upload'). Both the client and the
39
+ * sandbox read this field, so the mirror pair stays in sync. */
40
+ service?: string;
23
41
  /** Input message. */
24
42
  args: DtoMessage;
25
43
  /** Output message. */
@@ -29,9 +47,47 @@ export interface ThirdServiceMethodSchema extends SchemaBase {
29
47
  }
30
48
  /** Method input for defineThirdService: name/schema are set by the builder. */
31
49
  export type ThirdServiceMethodDef = Omit<ThirdServiceMethodSchema, 'type' | 'schema' | 'name'>;
50
+ /** An inbound callback of a third-party service. The third party pushes a
51
+ * payload (e.g. audit-result notify) to a platform endpoint; the platform
52
+ * verifies the third party's own signature/decrypts (hand-written in the
53
+ * generated controller body) and answers with `response`.
54
+ * `api` selects the backend the endpoint is generated into:
55
+ * {api}/src/modules/{thirdApi.name}/controller/ — the only module location
56
+ * whitelisted for @Public (lint controller), because a third party never
57
+ * holds an appKey. */
58
+ export interface ThirdCallbackSchema extends SchemaBase {
59
+ type: 'thirdCallback';
60
+ schema: ThirdServiceSchema;
61
+ /** Backend API the callback controller is generated into. */
62
+ api: ProjectApiSchema;
63
+ /** The third-party push service name (e.g. 'apply_notify', 'alter_notify'). */
64
+ service: string;
65
+ /** Push message (third party → platform). */
66
+ payload: DtoMessage;
67
+ /** Platform answer message (platform → third party). */
68
+ response: DtoMessage;
69
+ }
70
+ /** Callback input for defineThirdService: name/schema are set by the builder. */
71
+ export type ThirdCallbackDef = Omit<ThirdCallbackSchema, 'type' | 'schema' | 'name'>;
72
+ /** Builds one callback entry for `defineThirdService({ callbacks })`. The
73
+ * callback name (map key in the callbacks object) becomes the controller
74
+ * method name; everything else is declared here. */
75
+ export declare function defineThirdCallback(options: {
76
+ /** Backend API the callback controller is generated into. */
77
+ api: ProjectApiSchema;
78
+ /** The third-party push service name (e.g. 'apply_notify', 'alter_notify'). */
79
+ service: string;
80
+ /** Push message (third party → platform). */
81
+ payload: DtoMessage;
82
+ /** Platform answer message (platform → third party). */
83
+ response: DtoMessage;
84
+ description?: string;
85
+ }): ThirdCallbackDef;
32
86
  export declare function defineThirdService(options: {
33
87
  schema: ThirdApiSchema;
34
88
  name: string;
89
+ format?: ThirdServiceFormat;
35
90
  methods: Record<string, ThirdServiceMethodDef>;
91
+ callbacks?: Record<string, ThirdCallbackDef>;
36
92
  description?: string;
37
93
  }): ThirdServiceSchema;
@@ -1,10 +1,24 @@
1
+ /** Builds one callback entry for `defineThirdService({ callbacks })`. The
2
+ * callback name (map key in the callbacks object) becomes the controller
3
+ * method name; everything else is declared here. */
4
+ export function defineThirdCallback(options) {
5
+ return {
6
+ description: options.description,
7
+ api: options.api,
8
+ service: options.service,
9
+ payload: options.payload,
10
+ response: options.response,
11
+ };
12
+ }
1
13
  export function defineThirdService(options) {
2
14
  const schema = {
3
15
  type: 'thirdService',
4
16
  name: options.name,
5
17
  description: options.description,
6
18
  schema: options.schema,
19
+ format: options.format,
7
20
  methods: {},
21
+ callbacks: {},
8
22
  };
9
23
  for (const key of Object.keys(options.methods)) {
10
24
  const method = options.methods[key];
@@ -13,10 +27,24 @@ export function defineThirdService(options) {
13
27
  name: key,
14
28
  description: method.description,
15
29
  schema,
30
+ service: method.service,
16
31
  args: method.args,
17
32
  results: method.results,
18
33
  throws: method.throws,
19
34
  };
20
35
  }
36
+ for (const key of Object.keys(options.callbacks ?? {})) {
37
+ const callback = options.callbacks[key];
38
+ schema.callbacks[key] = {
39
+ type: 'thirdCallback',
40
+ name: key,
41
+ description: callback.description,
42
+ schema,
43
+ api: callback.api,
44
+ service: callback.service,
45
+ payload: callback.payload,
46
+ response: callback.response,
47
+ };
48
+ }
21
49
  return schema;
22
50
  }
@@ -0,0 +1,39 @@
1
+ import type { CollectionSchemaBase } from './dsl.js';
2
+ import type { DtoField } from './dto.js';
3
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
4
+ /** Session-credential columns the identity table must carry (hard constraint,
5
+ * decision #13): the token system writes token + refresh_token + login_at to
6
+ * the account table at login/refresh time. A table missing them cannot host
7
+ * an identity. */
8
+ export declare const TOKEN_CREDENTIAL_COLUMNS: readonly ['token', 'refresh_token', 'login_at'];
9
+ /** Built-in security-section field names: secret (signing, required) and
10
+ * cipher (channel encryption, optional). Generated at get-token time and
11
+ * stored in the Redis object — never backed by a table. */
12
+ export declare const TOKEN_SECURITY_FIELDS: readonly ['secret', 'cipher'];
13
+ export interface TokenSchema extends CollectionSchemaBase {
14
+ type: 'token';
15
+ /** The backend api module this token belongs to (shared instance from
16
+ * project.config.ts apis). Tokens are always backend-side. */
17
+ api: ProjectApiSchema;
18
+ /** The frontend app this token belongs to (shared instance from
19
+ * project.config). Required — an identity always belongs to one module. */
20
+ app: FrontAppSchema;
21
+ /** Security materials (present from get-token on): built-in secret
22
+ * (required, signing) + cipher (optional, channel encryption). Not backed
23
+ * by any table. */
24
+ security: Record<string, DtoField>;
25
+ /** Identity data (attached at login): fields projected from table columns
26
+ * via from(table, ...). Every source table must carry the session
27
+ * credential columns (TOKEN_CREDENTIAL_COLUMNS). */
28
+ identity: Record<string, DtoField>;
29
+ }
30
+ export declare function defineToken(options: {
31
+ name: string;
32
+ api: ProjectApiSchema;
33
+ app: FrontAppSchema;
34
+ identity: Record<string, DtoField>;
35
+ description?: string;
36
+ }): TokenSchema;
37
+ /** Structural check — TokenSchema instances may come from a different module
38
+ * copy, so instanceof is unreliable. */
39
+ export declare function isTokenSchema(v: unknown): v is TokenSchema;
package/dist/token.js ADDED
@@ -0,0 +1,96 @@
1
+ import { stringField } from './dsl.js';
2
+ import { dtoField } from './dto.js';
3
+ // Token = the server-side user identity object (login principal), a two-state
4
+ // object: `security` (built-in secret/cipher, present from get-token on) +
5
+ // `identity` (projected from table columns, attached at login). The client
6
+ // holds a pure random hash token referencing this object — never the object
7
+ // itself. Storage: token_schema/{api.name}/{app.name}/token/{name}.token.ts
8
+ // (one token per file), same layout as service_schema / dao_schema.
9
+ /** Session-credential columns the identity table must carry (hard constraint,
10
+ * decision #13): the token system writes token + refresh_token + login_at to
11
+ * the account table at login/refresh time. A table missing them cannot host
12
+ * an identity. */
13
+ export const TOKEN_CREDENTIAL_COLUMNS = ['token', 'refresh_token', 'login_at'];
14
+ /** Built-in security-section field names: secret (signing, required) and
15
+ * cipher (channel encryption, optional). Generated at get-token time and
16
+ * stored in the Redis object — never backed by a table. */
17
+ export const TOKEN_SECURITY_FIELDS = ['secret', 'cipher'];
18
+ /** Built-in security section: secret (required) + cipher (optional). */
19
+ function builtInSecurity() {
20
+ return {
21
+ secret: dtoField(stringField({ minLength: 32, maxLength: 64, optional: false, label: '签名密钥' })),
22
+ cipher: dtoField(stringField({ optional: true, label: '加密密钥' })),
23
+ };
24
+ }
25
+ function validateIdentityFields(tokenName, identity) {
26
+ const sourceTables = new Set();
27
+ for (const [key, f] of Object.entries(identity)) {
28
+ const source = f.field.schema;
29
+ if (source?.type !== 'table') {
30
+ throw new Error(`token ${tokenName}: identity field '${key}' must be projected from a table column (from(table, ...)), got a non-column field`);
31
+ }
32
+ if (TOKEN_SECURITY_FIELDS.includes(key)) {
33
+ throw new Error(`token ${tokenName}: identity field '${key}' collides with the built-in security field of the same name`);
34
+ }
35
+ sourceTables.add(source);
36
+ }
37
+ // Hard constraint (decision #13): every identity source table must carry
38
+ // the session credential columns — the token system writes them at
39
+ // login/refresh time, a table without them breaks the whole system.
40
+ for (const table of sourceTables) {
41
+ for (const col of TOKEN_CREDENTIAL_COLUMNS) {
42
+ if (table.columns[col] === undefined) {
43
+ throw new Error(`token ${tokenName}: identity table '${table.name}' must contain column '${col}' (hard constraint — the token system writes session credentials to the account table)`);
44
+ }
45
+ }
46
+ // Identity anchor (decision): every identity source table's primary key
47
+ // must be fully projected into identity — a partial key cannot uniquely
48
+ // locate the row (composite keys project every member).
49
+ const pkFields = table.primaryKey === undefined
50
+ ? []
51
+ : Array.isArray(table.primaryKey)
52
+ ? table.primaryKey
53
+ : [table.primaryKey];
54
+ for (const pk of pkFields) {
55
+ const projected = Object.values(identity).some((f) => f.field === pk);
56
+ if (!projected) {
57
+ throw new Error(`token ${tokenName}: identity must include the primary key column '${pk.name}' of table '${table.name}' (identity anchor — the user id is required)`);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ /** Write back the DTO field name from the map key (same convention as
63
+ * buildMessage — TokenSchema is a field container, consumers rely on
64
+ * field.name). */
65
+ function writeBackNames(segments) {
66
+ for (const segment of segments) {
67
+ for (const [key, df] of Object.entries(segment))
68
+ df.name = key;
69
+ }
70
+ }
71
+ export function defineToken(options) {
72
+ const { name, api, app, identity, description } = options;
73
+ if (!api.apps.includes(app)) {
74
+ throw new Error(`token ${name}: api '${api.name}' does not serve app '${app.name}'`);
75
+ }
76
+ validateIdentityFields(name, identity);
77
+ const security = builtInSecurity();
78
+ writeBackNames([security, identity]);
79
+ return {
80
+ type: 'token',
81
+ name,
82
+ description,
83
+ api,
84
+ app,
85
+ security,
86
+ identity,
87
+ };
88
+ }
89
+ /** Structural check — TokenSchema instances may come from a different module
90
+ * copy, so instanceof is unreliable. */
91
+ export function isTokenSchema(v) {
92
+ if (typeof v !== 'object' || v === null)
93
+ return false;
94
+ const vv = v;
95
+ return vv.type === 'token' && typeof vv.name === 'string';
96
+ }
@@ -1,4 +1,5 @@
1
1
  import { DtoMessage, ImportBase } from './dto.js';
2
+ import type { TokenSchema } from './token.js';
2
3
  export type EnumResolver = (enumName: string) => ImportBase | undefined;
3
4
  /** Collect all imports needed to render a DTO: include() bases + enum references. */
4
5
  export declare function collectDtoImports(schema: DtoMessage, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void;
@@ -6,6 +7,18 @@ export declare function collectDtoImports(schema: DtoMessage, resolver: EnumReso
6
7
  export declare function renderDtoExport(schema: DtoMessage, resolver: EnumResolver | undefined): string;
7
8
  /** Render the Static type export for a DTO. */
8
9
  export declare function renderDtoTypeExport(name: string): string;
10
+ /** Render one token export: a flat TypeBox object. Security fields keep
11
+ * their declared optionality (secret required, cipher optional); identity
12
+ * fields are Optional except the primary-key anchor — they only exist after
13
+ * login (two-state object, flat runtime shape), while the PK is guaranteed
14
+ * to be projected (token validation enforces it) and serves as the identity
15
+ * anchor (e.g. the tenant key for tenant-scoped controllers). */
16
+ export declare function renderTokenExport(token: TokenSchema, resolver: EnumResolver | undefined): string;
17
+ /** Render the Static type export for a token. */
18
+ export declare function renderTokenTypeExport(name: string): string;
19
+ /** Collect all imports needed to render a token: enum references across
20
+ * security + identity fields. */
21
+ export declare function collectTokenImports(token: TokenSchema, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void;
9
22
  export declare function renderDtoMessage(schema: DtoMessage, options?: {
10
23
  resolver?: EnumResolver;
11
24
  source?: string;
@@ -16,6 +16,20 @@ function fieldDescription(field) {
16
16
  function dtoFieldDescription(f) {
17
17
  return f.description ?? fieldDescription(f.field);
18
18
  }
19
+ /** Resolve a ref chain to its terminal field (the one without .ref).
20
+ * Cycles are a DSL definition error — fail loudly at render time. */
21
+ function resolveRefChain(f) {
22
+ const seen = new Set();
23
+ let cur = f;
24
+ while (cur.ref !== undefined) {
25
+ if (seen.has(cur.ref)) {
26
+ throw new Error(`dto field ${cur.name}: circular ref chain (field references itself)`);
27
+ }
28
+ seen.add(cur.ref);
29
+ cur = cur.ref;
30
+ }
31
+ return cur;
32
+ }
19
33
  function renderBasic(field, pattern, defaultValue, resolver, indent = 0, description) {
20
34
  if (pattern !== undefined && field.type !== 'string') {
21
35
  throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
@@ -152,10 +166,35 @@ function renderObject(fields, indent, resolver, description) {
152
166
  : obj;
153
167
  }
154
168
  function renderField(f, indent, resolver) {
169
+ // Ref branch renders its own optional (referencing overrides + chain fallback).
170
+ if (f.ref !== undefined)
171
+ return renderValue(f, indent, resolver);
155
172
  const base = renderValue(f, indent, resolver);
156
173
  return f.isOptional() ? `Type.Optional(${base})` : base;
157
174
  }
175
+ /** Render a ref-carrying field's bare type (no optional wrapper): resolve the
176
+ * chain, inherit the terminal field's type/constraints, keep the referencing
177
+ * field's own overrides (pattern / default / description). */
178
+ function renderRefBase(f, indent, resolver) {
179
+ const target = resolveRefChain(f);
180
+ const targetField = target.field;
181
+ const pattern = f.pattern ?? target.pattern;
182
+ const defaultValue = f.default ?? target.default;
183
+ const desc = f.description ?? dtoFieldDescription(target);
184
+ return renderBasic(targetField, pattern, defaultValue, resolver, indent, desc);
185
+ }
186
+ /** Render a ref-carrying field with its optional wrapper: referencing
187
+ * override first, then the chain's DtoField-level optional, then the bare
188
+ * column optionality. */
189
+ function renderRefField(f, indent, resolver) {
190
+ const target = resolveRefChain(f);
191
+ const optional = f.optional ?? target.optional ?? target.field.optional ?? false;
192
+ const base = renderRefBase(f, indent, resolver);
193
+ return optional ? `Type.Optional(${base})` : base;
194
+ }
158
195
  function renderValue(f, indent, resolver) {
196
+ if (f.ref !== undefined)
197
+ return renderRefField(f, indent, resolver);
159
198
  if (f.field.type === 'array') {
160
199
  const items = f.field.items;
161
200
  const desc = dtoFieldDescription(f);
@@ -185,6 +224,10 @@ function renderValue(f, indent, resolver) {
185
224
  return renderBasic(f.field, f.pattern, f.default, resolver, indent, dtoFieldDescription(f));
186
225
  }
187
226
  function collectEnumImports(f, resolver, out) {
227
+ if (f.ref !== undefined) {
228
+ collectEnumImports(resolveRefChain(f), resolver, out);
229
+ return;
230
+ }
188
231
  if (f.field.type === 'array') {
189
232
  const items = f.field.items;
190
233
  if (isDtoMessage(items))
@@ -227,19 +270,91 @@ export function collectDtoImports(schema, resolver, out) {
227
270
  for (const f of Object.values(schema.fields))
228
271
  collectEnumImports(f, resolver, out);
229
272
  }
273
+ /** Render the server-injection base: token-injected fields as Optional
274
+ * properties of a TypeBox object, plus a non-enumerable __inject adapter
275
+ * (same mechanism as hand-written bases, see pylon __inject docs) that fills
276
+ * each field from the token at runtime. */
277
+ function renderInjectBase(fields, resolver) {
278
+ const entries = Object.entries(fields).map(([name, f]) => {
279
+ const base = f.ref !== undefined ? renderRefBase(f, 1, resolver) : renderValue(f, 1, resolver);
280
+ return ` ${name}: Type.Optional(${base})`;
281
+ });
282
+ const inner = `Type.Object({\n${entries.join(',\n')}\n})`;
283
+ const assigns = Object.keys(fields)
284
+ .map((k) => `body.${k} = token.${k};`)
285
+ .join(' ');
286
+ return [
287
+ `Object.defineProperty(`,
288
+ ` ${inner},`,
289
+ ` '__inject',`,
290
+ ` { value: (body: Record<string, unknown>, token: Record<string, unknown>): void => { ${assigns} }, enumerable: false },`,
291
+ `)`,
292
+ ].join('\n');
293
+ }
230
294
  /** Render one DTO export (const + type) — no file header, for file-level generation. */
231
295
  export function renderDtoExport(schema, resolver) {
232
- const object = renderObject(schema.fields, 1, resolver, schema.description);
233
- const bases = schema.bases ?? [];
234
- const body = bases.length > 0
235
- ? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
236
- : object;
296
+ const injectFields = {};
297
+ const normalFields = {};
298
+ for (const [key, f] of Object.entries(schema.fields)) {
299
+ if (f.injectFrom !== undefined)
300
+ injectFields[key] = f;
301
+ else
302
+ normalFields[key] = f;
303
+ }
304
+ const parts = [];
305
+ if (Object.keys(injectFields).length > 0)
306
+ parts.push(renderInjectBase(injectFields, resolver));
307
+ parts.push(renderObject(normalFields, 1, resolver, schema.description));
308
+ for (const base of schema.bases ?? [])
309
+ parts.push(renderBase(base));
310
+ const body = parts.length > 1 ? `Type.Intersect([${parts.join(', ')}])` : parts[0];
237
311
  return `export const ${schema.name} = ${body};`;
238
312
  }
239
313
  /** Render the Static type export for a DTO. */
240
314
  export function renderDtoTypeExport(name) {
241
315
  return `export type ${name} = Static<typeof ${name}>;`;
242
316
  }
317
+ /** Render one token export: a flat TypeBox object. Security fields keep
318
+ * their declared optionality (secret required, cipher optional); identity
319
+ * fields are Optional except the primary-key anchor — they only exist after
320
+ * login (two-state object, flat runtime shape), while the PK is guaranteed
321
+ * to be projected (token validation enforces it) and serves as the identity
322
+ * anchor (e.g. the tenant key for tenant-scoped controllers). */
323
+ export function renderTokenExport(token, resolver) {
324
+ const constName = `${token.name}Token`;
325
+ const entries = [
326
+ ...Object.entries(token.security).map(([name, f]) => ` ${name}: ${renderField(f, 1, resolver)}`),
327
+ ...Object.entries(token.identity).map(([name, f]) => isPrimaryKeyProjection(f) ? ` ${name}: ${renderValue(f, 1, resolver)}` : ` ${name}: Type.Optional(${renderValue(f, 1, resolver)})`),
328
+ ];
329
+ const desc = token.description !== undefined ? `, { description: ${renderString(token.description)} }` : '';
330
+ return `export const ${constName} = Type.Object({\n${entries.join(',\n')}\n}${desc});`;
331
+ }
332
+ /** True when the DTO field projects a primary-key column of its source table. */
333
+ function isPrimaryKeyProjection(f) {
334
+ const tbl = f.field.schema;
335
+ if (tbl?.type !== 'table')
336
+ return false;
337
+ // A table-backed field is always a plain Field (only Field carries a table
338
+ // schema); array/object DTO fields reference messages, not tables.
339
+ const field = f.field;
340
+ const table = tbl;
341
+ const pk = table.primaryKey;
342
+ if (!pk)
343
+ return false;
344
+ const pks = Array.isArray(pk) ? pk : [pk];
345
+ return pks.includes(field);
346
+ }
347
+ /** Render the Static type export for a token. */
348
+ export function renderTokenTypeExport(name) {
349
+ return `export type ${name}Token = Static<typeof ${name}Token>;`;
350
+ }
351
+ /** Collect all imports needed to render a token: enum references across
352
+ * security + identity fields. */
353
+ export function collectTokenImports(token, resolver, out) {
354
+ for (const f of [...Object.values(token.security), ...Object.values(token.identity)]) {
355
+ collectEnumImports(f, resolver, out);
356
+ }
357
+ }
243
358
  export function renderDtoMessage(schema, options = {}) {
244
359
  const { resolver, source } = options;
245
360
  const imports = new Map();