@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 +4 -5
- package/dist/dto.d.ts +13 -0
- package/dist/dto.js +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mysql-driver.js +3 -3
- package/dist/service.js +3 -0
- package/dist/third-service.d.ts +58 -2
- package/dist/third-service.js +28 -0
- package/dist/token.d.ts +39 -0
- package/dist/token.js +96 -0
- package/dist/typebox-driver.d.ts +13 -0
- package/dist/typebox-driver.js +120 -5
- package/docs/curd.md +150 -146
- package/docs/gen-login.md +136 -0
- package/docs/third-service.md +201 -151
- package/docs/token-migration.md +60 -0
- package/docs/token.md +341 -327
- package/docs/wechat.md +235 -0
- package/package.json +2 -2
- package/src/convert.ts +9 -9
- package/src/dto.ts +364 -331
- package/src/index.ts +1 -0
- package/src/mysql-driver.ts +108 -108
- package/src/service.ts +5 -0
- package/src/third-service.ts +86 -2
- package/src/token.ts +139 -0
- package/src/typebox-driver.ts +128 -6
package/src/typebox-driver.ts
CHANGED
|
@@ -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
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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 } = {},
|