@atscript/typescript 0.1.29 → 0.1.31

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/cli.cjs CHANGED
@@ -289,8 +289,9 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
289
289
  if (isGrp) this.write(")");
290
290
  return this.write("[]");
291
291
  }
292
+ if ((0, __atscript_core.isPrimitive)(def)) return this.write(renderPrimitiveTypeDef(def.config.type));
292
293
  }
293
- renderStructure(struct, asClass) {
294
+ renderStructure(struct, asClass, interfaceNode) {
294
295
  this.blockln("{}");
295
296
  const patterns = [];
296
297
  const propsDefs = new Set();
@@ -336,6 +337,7 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
336
337
  this.writeln("static toJsonSchema: () => any");
337
338
  if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
338
339
  this.writeln("static toExampleData?: () => any");
340
+ if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
339
341
  }
340
342
  this.pop();
341
343
  }
@@ -344,12 +346,63 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
344
346
  const exported = node.token("export")?.text === "export";
345
347
  this.renderJsDoc(node);
346
348
  this.write(exported ? "export declare " : "declare ");
347
- this.write(`class ${node.id} `);
348
- const struct = node.getDefinition();
349
- if (struct?.entity === "structure") this.renderStructure(struct, node.id);
349
+ if (node.hasExtends) {
350
+ const firstParent = node.extendsTokens[0].text;
351
+ this.write(`class ${node.id} extends ${firstParent} `);
352
+ const resolved = this.doc.resolveInterfaceExtends(node);
353
+ if (resolved?.entity === "structure") {
354
+ const firstParentUnwound = this.doc.unwindType(firstParent);
355
+ let firstParentProps;
356
+ if (firstParentUnwound?.def) {
357
+ let fpDef = firstParentUnwound.def;
358
+ if ((0, __atscript_core.isInterface)(fpDef)) {
359
+ if (fpDef.hasExtends) {
360
+ const fpResolved = firstParentUnwound.doc.resolveInterfaceExtends(fpDef);
361
+ if (fpResolved && (0, __atscript_core.isStructure)(fpResolved)) firstParentProps = fpResolved.props;
362
+ }
363
+ if (!firstParentProps) fpDef = fpDef.getDefinition() || fpDef;
364
+ }
365
+ if (!firstParentProps && (0, __atscript_core.isStructure)(fpDef)) firstParentProps = fpDef.props;
366
+ }
367
+ this.renderStructureFiltered(resolved, node.id, firstParentProps, node);
368
+ } else this.writeln("{}");
369
+ } else {
370
+ this.write(`class ${node.id} `);
371
+ const struct = node.getDefinition();
372
+ if (struct?.entity === "structure") this.renderStructure(struct, node.id, node);
350
373
  else this.writeln("{}");
374
+ }
351
375
  this.writeln();
352
376
  }
377
+ /**
378
+ * Renders a structure block, optionally filtering out props that exist in a parent.
379
+ */ renderStructureFiltered(struct, asClass, filterProps, interfaceNode) {
380
+ if (!filterProps) return this.renderStructure(struct, asClass, interfaceNode);
381
+ this.blockln("{}");
382
+ for (const prop of Array.from(struct.props.values())) {
383
+ if (filterProps.has(prop.id)) continue;
384
+ if (prop.token("identifier")?.pattern) continue;
385
+ const phantomType = this.phantomPropType(prop.getDefinition());
386
+ if (phantomType) {
387
+ this.writeln(`// ${prop.id}: ${phantomType}`);
388
+ continue;
389
+ }
390
+ const optional = !!prop.token("optional");
391
+ this.write(wrapProp(prop.id), optional ? "?" : "", ": ");
392
+ const renderedDef = this.renderTypeDefString(prop.getDefinition());
393
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
394
+ }
395
+ this.writeln("static __is_atscript_annotated_type: true");
396
+ this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
397
+ this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
398
+ this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
399
+ if (resolveJsonSchemaMode(this.opts) === false) this.writeln("/** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */");
400
+ this.writeln("static toJsonSchema: () => any");
401
+ if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
402
+ this.writeln("static toExampleData?: () => any");
403
+ if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
404
+ this.pop();
405
+ }
353
406
  renderType(node) {
354
407
  this.writeln();
355
408
  const exported = node.token("export")?.text === "export";
@@ -403,6 +456,50 @@ else if ((0, __atscript_core.isPrimitive)(realDef)) typeDef = `TAtscriptTypeFina
403
456
  this.writeln("const toExampleData: (() => any) | undefined");
404
457
  this.popln();
405
458
  }
459
+ /**
460
+ * Checks whether an interface has the `@db.table` annotation.
461
+ *
462
+ * NOTE: Only `@db.table` interfaces get the `__flat` static property.
463
+ * This is intentionally hardcoded — `__flat` exists solely to improve
464
+ * type-safety for filter expressions and `$select`/`$sort` operations
465
+ * in the DB layer. Non-DB interfaces have no use for dot-notation path types.
466
+ */ hasDbTable(node) {
467
+ return !!node.annotations?.some((a) => a.name === "db.table");
468
+ }
469
+ /**
470
+ * Renders the `static __flat` property — a map of all dot-notation paths
471
+ * to their TypeScript value types.
472
+ *
473
+ * This enables type-safe autocomplete for filter keys and `$select`/`$sort`
474
+ * paths when using `AtscriptDbTable`. The `FlatOf<T>` utility type extracts
475
+ * this map at the type level.
476
+ *
477
+ * Special rendering rules:
478
+ * - **Intermediate paths** (structures, arrays of structures) → `never`
479
+ * (prevents `$eq` comparisons, but allows `$select` and `$exists`)
480
+ * - **`@db.json` fields** → `string` (stored as serialized JSON in DB,
481
+ * not individually queryable)
482
+ * - **Leaf fields** → their original TypeScript type
483
+ */ renderFlat(node) {
484
+ const flatMap = (0, __atscript_core.flattenInterfaceNode)(this.doc, node);
485
+ if (flatMap.size === 0) return;
486
+ this.write("static __flat: ");
487
+ this.blockln("{}");
488
+ for (const [path$3, descriptor] of flatMap) {
489
+ this.write(`"${escapeQuotes(path$3)}"`);
490
+ if (descriptor.optional) this.write("?");
491
+ this.write(": ");
492
+ if (descriptor.intermediate) this.writeln("never");
493
+ else if (descriptor.dbJson) this.writeln("string");
494
+ else {
495
+ const originalDef = descriptor.propNode?.getDefinition();
496
+ const defToRender = originalDef && !((0, __atscript_core.isGroup)(descriptor.def) && descriptor.def !== originalDef) ? originalDef : descriptor.def;
497
+ const renderedDef = this.renderTypeDefString(defToRender);
498
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
499
+ }
500
+ }
501
+ this.pop();
502
+ }
406
503
  phantomPropType(def) {
407
504
  if (!def) return undefined;
408
505
  if ((0, __atscript_core.isPrimitive)(def) && def.config.type === "phantom") return def.id;
@@ -1214,7 +1311,11 @@ else {
1214
1311
  const unwound = this.doc.unwindType(annotateNode.targetName);
1215
1312
  if (unwound?.def) {
1216
1313
  let def = this.doc.mergeIntersection(unwound.def);
1217
- if ((0, __atscript_core.isInterface)(def)) def = def.getDefinition() || def;
1314
+ if ((0, __atscript_core.isInterface)(def)) if (def.hasExtends) {
1315
+ const resolved = unwound.doc.resolveInterfaceExtends(def);
1316
+ if (resolved) def = resolved;
1317
+ else def = def.getDefinition() || def;
1318
+ } else def = def.getDefinition() || def;
1218
1319
  this._adHocAnnotations = this.buildAdHocMap([annotateNode]);
1219
1320
  this.annotateType(def, node.id);
1220
1321
  this._adHocAnnotations = null;
@@ -1225,7 +1326,12 @@ else {
1225
1326
  }
1226
1327
  }
1227
1328
  } else {
1228
- this.annotateType(node.getDefinition(), node.id);
1329
+ let def = node.getDefinition();
1330
+ if ((0, __atscript_core.isInterface)(node) && node.hasExtends) {
1331
+ const resolved = this.doc.resolveInterfaceExtends(node);
1332
+ if (resolved) def = resolved;
1333
+ }
1334
+ this.annotateType(def, node.id);
1229
1335
  this.indent().defineMetadata(node).unindent();
1230
1336
  this.writeln();
1231
1337
  }
@@ -1314,7 +1420,11 @@ else {
1314
1420
  switch (node.entity) {
1315
1421
  case "interface":
1316
1422
  case "type": {
1317
- const def = node.getDefinition();
1423
+ let def = node.getDefinition();
1424
+ if ((0, __atscript_core.isInterface)(node) && node.hasExtends) {
1425
+ const resolved = this.doc.resolveInterfaceExtends(node);
1426
+ if (resolved) def = resolved;
1427
+ }
1318
1428
  const handle = this.toAnnotatedHandle(def, true);
1319
1429
  const typeId = this.typeIds.get(node) ?? (node.__typeId !== null && node.__typeId !== undefined ? node.id : undefined);
1320
1430
  if (typeId) handle.id(typeId);
package/dist/index.cjs CHANGED
@@ -287,8 +287,9 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
287
287
  if (isGrp) this.write(")");
288
288
  return this.write("[]");
289
289
  }
290
+ if ((0, __atscript_core.isPrimitive)(def)) return this.write(renderPrimitiveTypeDef(def.config.type));
290
291
  }
291
- renderStructure(struct, asClass) {
292
+ renderStructure(struct, asClass, interfaceNode) {
292
293
  this.blockln("{}");
293
294
  const patterns = [];
294
295
  const propsDefs = new Set();
@@ -334,6 +335,7 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
334
335
  this.writeln("static toJsonSchema: () => any");
335
336
  if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
336
337
  this.writeln("static toExampleData?: () => any");
338
+ if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
337
339
  }
338
340
  this.pop();
339
341
  }
@@ -342,12 +344,63 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
342
344
  const exported = node.token("export")?.text === "export";
343
345
  this.renderJsDoc(node);
344
346
  this.write(exported ? "export declare " : "declare ");
345
- this.write(`class ${node.id} `);
346
- const struct = node.getDefinition();
347
- if (struct?.entity === "structure") this.renderStructure(struct, node.id);
347
+ if (node.hasExtends) {
348
+ const firstParent = node.extendsTokens[0].text;
349
+ this.write(`class ${node.id} extends ${firstParent} `);
350
+ const resolved = this.doc.resolveInterfaceExtends(node);
351
+ if (resolved?.entity === "structure") {
352
+ const firstParentUnwound = this.doc.unwindType(firstParent);
353
+ let firstParentProps;
354
+ if (firstParentUnwound?.def) {
355
+ let fpDef = firstParentUnwound.def;
356
+ if ((0, __atscript_core.isInterface)(fpDef)) {
357
+ if (fpDef.hasExtends) {
358
+ const fpResolved = firstParentUnwound.doc.resolveInterfaceExtends(fpDef);
359
+ if (fpResolved && (0, __atscript_core.isStructure)(fpResolved)) firstParentProps = fpResolved.props;
360
+ }
361
+ if (!firstParentProps) fpDef = fpDef.getDefinition() || fpDef;
362
+ }
363
+ if (!firstParentProps && (0, __atscript_core.isStructure)(fpDef)) firstParentProps = fpDef.props;
364
+ }
365
+ this.renderStructureFiltered(resolved, node.id, firstParentProps, node);
366
+ } else this.writeln("{}");
367
+ } else {
368
+ this.write(`class ${node.id} `);
369
+ const struct = node.getDefinition();
370
+ if (struct?.entity === "structure") this.renderStructure(struct, node.id, node);
348
371
  else this.writeln("{}");
372
+ }
349
373
  this.writeln();
350
374
  }
375
+ /**
376
+ * Renders a structure block, optionally filtering out props that exist in a parent.
377
+ */ renderStructureFiltered(struct, asClass, filterProps, interfaceNode) {
378
+ if (!filterProps) return this.renderStructure(struct, asClass, interfaceNode);
379
+ this.blockln("{}");
380
+ for (const prop of Array.from(struct.props.values())) {
381
+ if (filterProps.has(prop.id)) continue;
382
+ if (prop.token("identifier")?.pattern) continue;
383
+ const phantomType = this.phantomPropType(prop.getDefinition());
384
+ if (phantomType) {
385
+ this.writeln(`// ${prop.id}: ${phantomType}`);
386
+ continue;
387
+ }
388
+ const optional = !!prop.token("optional");
389
+ this.write(wrapProp(prop.id), optional ? "?" : "", ": ");
390
+ const renderedDef = this.renderTypeDefString(prop.getDefinition());
391
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
392
+ }
393
+ this.writeln("static __is_atscript_annotated_type: true");
394
+ this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
395
+ this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
396
+ this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
397
+ if (resolveJsonSchemaMode(this.opts) === false) this.writeln("/** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */");
398
+ this.writeln("static toJsonSchema: () => any");
399
+ if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
400
+ this.writeln("static toExampleData?: () => any");
401
+ if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
402
+ this.pop();
403
+ }
351
404
  renderType(node) {
352
405
  this.writeln();
353
406
  const exported = node.token("export")?.text === "export";
@@ -401,6 +454,50 @@ else if ((0, __atscript_core.isPrimitive)(realDef)) typeDef = `TAtscriptTypeFina
401
454
  this.writeln("const toExampleData: (() => any) | undefined");
402
455
  this.popln();
403
456
  }
457
+ /**
458
+ * Checks whether an interface has the `@db.table` annotation.
459
+ *
460
+ * NOTE: Only `@db.table` interfaces get the `__flat` static property.
461
+ * This is intentionally hardcoded — `__flat` exists solely to improve
462
+ * type-safety for filter expressions and `$select`/`$sort` operations
463
+ * in the DB layer. Non-DB interfaces have no use for dot-notation path types.
464
+ */ hasDbTable(node) {
465
+ return !!node.annotations?.some((a) => a.name === "db.table");
466
+ }
467
+ /**
468
+ * Renders the `static __flat` property — a map of all dot-notation paths
469
+ * to their TypeScript value types.
470
+ *
471
+ * This enables type-safe autocomplete for filter keys and `$select`/`$sort`
472
+ * paths when using `AtscriptDbTable`. The `FlatOf<T>` utility type extracts
473
+ * this map at the type level.
474
+ *
475
+ * Special rendering rules:
476
+ * - **Intermediate paths** (structures, arrays of structures) → `never`
477
+ * (prevents `$eq` comparisons, but allows `$select` and `$exists`)
478
+ * - **`@db.json` fields** → `string` (stored as serialized JSON in DB,
479
+ * not individually queryable)
480
+ * - **Leaf fields** → their original TypeScript type
481
+ */ renderFlat(node) {
482
+ const flatMap = (0, __atscript_core.flattenInterfaceNode)(this.doc, node);
483
+ if (flatMap.size === 0) return;
484
+ this.write("static __flat: ");
485
+ this.blockln("{}");
486
+ for (const [path$2, descriptor] of flatMap) {
487
+ this.write(`"${escapeQuotes(path$2)}"`);
488
+ if (descriptor.optional) this.write("?");
489
+ this.write(": ");
490
+ if (descriptor.intermediate) this.writeln("never");
491
+ else if (descriptor.dbJson) this.writeln("string");
492
+ else {
493
+ const originalDef = descriptor.propNode?.getDefinition();
494
+ const defToRender = originalDef && !((0, __atscript_core.isGroup)(descriptor.def) && descriptor.def !== originalDef) ? originalDef : descriptor.def;
495
+ const renderedDef = this.renderTypeDefString(defToRender);
496
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
497
+ }
498
+ }
499
+ this.pop();
500
+ }
404
501
  phantomPropType(def) {
405
502
  if (!def) return undefined;
406
503
  if ((0, __atscript_core.isPrimitive)(def) && def.config.type === "phantom") return def.id;
@@ -1212,7 +1309,11 @@ else {
1212
1309
  const unwound = this.doc.unwindType(annotateNode.targetName);
1213
1310
  if (unwound?.def) {
1214
1311
  let def = this.doc.mergeIntersection(unwound.def);
1215
- if ((0, __atscript_core.isInterface)(def)) def = def.getDefinition() || def;
1312
+ if ((0, __atscript_core.isInterface)(def)) if (def.hasExtends) {
1313
+ const resolved = unwound.doc.resolveInterfaceExtends(def);
1314
+ if (resolved) def = resolved;
1315
+ else def = def.getDefinition() || def;
1316
+ } else def = def.getDefinition() || def;
1216
1317
  this._adHocAnnotations = this.buildAdHocMap([annotateNode]);
1217
1318
  this.annotateType(def, node.id);
1218
1319
  this._adHocAnnotations = null;
@@ -1223,7 +1324,12 @@ else {
1223
1324
  }
1224
1325
  }
1225
1326
  } else {
1226
- this.annotateType(node.getDefinition(), node.id);
1327
+ let def = node.getDefinition();
1328
+ if ((0, __atscript_core.isInterface)(node) && node.hasExtends) {
1329
+ const resolved = this.doc.resolveInterfaceExtends(node);
1330
+ if (resolved) def = resolved;
1331
+ }
1332
+ this.annotateType(def, node.id);
1227
1333
  this.indent().defineMetadata(node).unindent();
1228
1334
  this.writeln();
1229
1335
  }
@@ -1312,7 +1418,11 @@ else {
1312
1418
  switch (node.entity) {
1313
1419
  case "interface":
1314
1420
  case "type": {
1315
- const def = node.getDefinition();
1421
+ let def = node.getDefinition();
1422
+ if ((0, __atscript_core.isInterface)(node) && node.hasExtends) {
1423
+ const resolved = this.doc.resolveInterfaceExtends(node);
1424
+ if (resolved) def = resolved;
1425
+ }
1316
1426
  const handle = this.toAnnotatedHandle(def, true);
1317
1427
  const typeId = this.typeIds.get(node) ?? (node.__typeId !== null && node.__typeId !== undefined ? node.id : undefined);
1318
1428
  if (typeId) handle.id(typeId);
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from "path";
2
- import { DEFAULT_FORMAT, isArray, isConst, isGroup, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
2
+ import { DEFAULT_FORMAT, flattenInterfaceNode, isArray, isConst, isGroup, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
3
3
 
4
4
  //#region packages/typescript/src/codegen/code-printer.ts
5
5
  function _define_property$4(obj, key, value) {
@@ -262,8 +262,9 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
262
262
  if (isGrp) this.write(")");
263
263
  return this.write("[]");
264
264
  }
265
+ if (isPrimitive(def)) return this.write(renderPrimitiveTypeDef(def.config.type));
265
266
  }
266
- renderStructure(struct, asClass) {
267
+ renderStructure(struct, asClass, interfaceNode) {
267
268
  this.blockln("{}");
268
269
  const patterns = [];
269
270
  const propsDefs = new Set();
@@ -309,6 +310,7 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
309
310
  this.writeln("static toJsonSchema: () => any");
310
311
  if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
311
312
  this.writeln("static toExampleData?: () => any");
313
+ if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
312
314
  }
313
315
  this.pop();
314
316
  }
@@ -317,12 +319,63 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
317
319
  const exported = node.token("export")?.text === "export";
318
320
  this.renderJsDoc(node);
319
321
  this.write(exported ? "export declare " : "declare ");
320
- this.write(`class ${node.id} `);
321
- const struct = node.getDefinition();
322
- if (struct?.entity === "structure") this.renderStructure(struct, node.id);
322
+ if (node.hasExtends) {
323
+ const firstParent = node.extendsTokens[0].text;
324
+ this.write(`class ${node.id} extends ${firstParent} `);
325
+ const resolved = this.doc.resolveInterfaceExtends(node);
326
+ if (resolved?.entity === "structure") {
327
+ const firstParentUnwound = this.doc.unwindType(firstParent);
328
+ let firstParentProps;
329
+ if (firstParentUnwound?.def) {
330
+ let fpDef = firstParentUnwound.def;
331
+ if (isInterface(fpDef)) {
332
+ if (fpDef.hasExtends) {
333
+ const fpResolved = firstParentUnwound.doc.resolveInterfaceExtends(fpDef);
334
+ if (fpResolved && isStructure(fpResolved)) firstParentProps = fpResolved.props;
335
+ }
336
+ if (!firstParentProps) fpDef = fpDef.getDefinition() || fpDef;
337
+ }
338
+ if (!firstParentProps && isStructure(fpDef)) firstParentProps = fpDef.props;
339
+ }
340
+ this.renderStructureFiltered(resolved, node.id, firstParentProps, node);
341
+ } else this.writeln("{}");
342
+ } else {
343
+ this.write(`class ${node.id} `);
344
+ const struct = node.getDefinition();
345
+ if (struct?.entity === "structure") this.renderStructure(struct, node.id, node);
323
346
  else this.writeln("{}");
347
+ }
324
348
  this.writeln();
325
349
  }
350
+ /**
351
+ * Renders a structure block, optionally filtering out props that exist in a parent.
352
+ */ renderStructureFiltered(struct, asClass, filterProps, interfaceNode) {
353
+ if (!filterProps) return this.renderStructure(struct, asClass, interfaceNode);
354
+ this.blockln("{}");
355
+ for (const prop of Array.from(struct.props.values())) {
356
+ if (filterProps.has(prop.id)) continue;
357
+ if (prop.token("identifier")?.pattern) continue;
358
+ const phantomType = this.phantomPropType(prop.getDefinition());
359
+ if (phantomType) {
360
+ this.writeln(`// ${prop.id}: ${phantomType}`);
361
+ continue;
362
+ }
363
+ const optional = !!prop.token("optional");
364
+ this.write(wrapProp(prop.id), optional ? "?" : "", ": ");
365
+ const renderedDef = this.renderTypeDefString(prop.getDefinition());
366
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
367
+ }
368
+ this.writeln("static __is_atscript_annotated_type: true");
369
+ this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
370
+ this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
371
+ this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
372
+ if (resolveJsonSchemaMode(this.opts) === false) this.writeln("/** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */");
373
+ this.writeln("static toJsonSchema: () => any");
374
+ if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
375
+ this.writeln("static toExampleData?: () => any");
376
+ if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
377
+ this.pop();
378
+ }
326
379
  renderType(node) {
327
380
  this.writeln();
328
381
  const exported = node.token("export")?.text === "export";
@@ -376,6 +429,50 @@ else if (isPrimitive(realDef)) typeDef = `TAtscriptTypeFinal<${name}>`;
376
429
  this.writeln("const toExampleData: (() => any) | undefined");
377
430
  this.popln();
378
431
  }
432
+ /**
433
+ * Checks whether an interface has the `@db.table` annotation.
434
+ *
435
+ * NOTE: Only `@db.table` interfaces get the `__flat` static property.
436
+ * This is intentionally hardcoded — `__flat` exists solely to improve
437
+ * type-safety for filter expressions and `$select`/`$sort` operations
438
+ * in the DB layer. Non-DB interfaces have no use for dot-notation path types.
439
+ */ hasDbTable(node) {
440
+ return !!node.annotations?.some((a) => a.name === "db.table");
441
+ }
442
+ /**
443
+ * Renders the `static __flat` property — a map of all dot-notation paths
444
+ * to their TypeScript value types.
445
+ *
446
+ * This enables type-safe autocomplete for filter keys and `$select`/`$sort`
447
+ * paths when using `AtscriptDbTable`. The `FlatOf<T>` utility type extracts
448
+ * this map at the type level.
449
+ *
450
+ * Special rendering rules:
451
+ * - **Intermediate paths** (structures, arrays of structures) → `never`
452
+ * (prevents `$eq` comparisons, but allows `$select` and `$exists`)
453
+ * - **`@db.json` fields** → `string` (stored as serialized JSON in DB,
454
+ * not individually queryable)
455
+ * - **Leaf fields** → their original TypeScript type
456
+ */ renderFlat(node) {
457
+ const flatMap = flattenInterfaceNode(this.doc, node);
458
+ if (flatMap.size === 0) return;
459
+ this.write("static __flat: ");
460
+ this.blockln("{}");
461
+ for (const [path$1, descriptor] of flatMap) {
462
+ this.write(`"${escapeQuotes(path$1)}"`);
463
+ if (descriptor.optional) this.write("?");
464
+ this.write(": ");
465
+ if (descriptor.intermediate) this.writeln("never");
466
+ else if (descriptor.dbJson) this.writeln("string");
467
+ else {
468
+ const originalDef = descriptor.propNode?.getDefinition();
469
+ const defToRender = originalDef && !(isGroup(descriptor.def) && descriptor.def !== originalDef) ? originalDef : descriptor.def;
470
+ const renderedDef = this.renderTypeDefString(defToRender);
471
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
472
+ }
473
+ }
474
+ this.pop();
475
+ }
379
476
  phantomPropType(def) {
380
477
  if (!def) return undefined;
381
478
  if (isPrimitive(def) && def.config.type === "phantom") return def.id;
@@ -1187,7 +1284,11 @@ else {
1187
1284
  const unwound = this.doc.unwindType(annotateNode.targetName);
1188
1285
  if (unwound?.def) {
1189
1286
  let def = this.doc.mergeIntersection(unwound.def);
1190
- if (isInterface(def)) def = def.getDefinition() || def;
1287
+ if (isInterface(def)) if (def.hasExtends) {
1288
+ const resolved = unwound.doc.resolveInterfaceExtends(def);
1289
+ if (resolved) def = resolved;
1290
+ else def = def.getDefinition() || def;
1291
+ } else def = def.getDefinition() || def;
1191
1292
  this._adHocAnnotations = this.buildAdHocMap([annotateNode]);
1192
1293
  this.annotateType(def, node.id);
1193
1294
  this._adHocAnnotations = null;
@@ -1198,7 +1299,12 @@ else {
1198
1299
  }
1199
1300
  }
1200
1301
  } else {
1201
- this.annotateType(node.getDefinition(), node.id);
1302
+ let def = node.getDefinition();
1303
+ if (isInterface(node) && node.hasExtends) {
1304
+ const resolved = this.doc.resolveInterfaceExtends(node);
1305
+ if (resolved) def = resolved;
1306
+ }
1307
+ this.annotateType(def, node.id);
1202
1308
  this.indent().defineMetadata(node).unindent();
1203
1309
  this.writeln();
1204
1310
  }
@@ -1287,7 +1393,11 @@ else {
1287
1393
  switch (node.entity) {
1288
1394
  case "interface":
1289
1395
  case "type": {
1290
- const def = node.getDefinition();
1396
+ let def = node.getDefinition();
1397
+ if (isInterface(node) && node.hasExtends) {
1398
+ const resolved = this.doc.resolveInterfaceExtends(node);
1399
+ if (resolved) def = resolved;
1400
+ }
1291
1401
  const handle = this.toAnnotatedHandle(def, true);
1292
1402
  const typeId = this.typeIds.get(node) ?? (node.__typeId !== null && node.__typeId !== undefined ? node.id : undefined);
1293
1403
  if (typeId) handle.id(typeId);
package/dist/utils.d.ts CHANGED
@@ -48,7 +48,7 @@ interface TValidatorPluginContext {
48
48
  * @typeParam T - The annotated type definition.
49
49
  * @typeParam DataType - The TypeScript type that `validate` narrows to (auto-inferred).
50
50
  */
51
- declare class Validator<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> {
51
+ declare class Validator<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType$1<T>> {
52
52
  protected readonly def: T;
53
53
  protected opts: TValidatorOptions;
54
54
  constructor(def: T, opts?: Partial<TValidatorOptions>);
@@ -159,7 +159,7 @@ type InferDataType<T> = T extends {
159
159
  * type Data = TAtscriptDataType<typeof MyInterface>
160
160
  * ```
161
161
  */
162
- type TAtscriptDataType<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> = T extends {
162
+ type TAtscriptDataType$1<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> = T extends {
163
163
  type: {
164
164
  __dataType?: infer D;
165
165
  };
@@ -535,5 +535,16 @@ declare function serializeAnnotatedType(type: TAtscriptAnnotatedType, options?:
535
535
  */
536
536
  declare function deserializeAnnotatedType(data: TSerializedAnnotatedType): TAtscriptAnnotatedType;
537
537
 
538
+ /**
539
+ * Extracts the flat dot-notation type map from an Atscript annotated type.
540
+ * If the type has a `__flat` static property (emitted for `@db.table` interfaces),
541
+ * returns that flat map. Otherwise falls back to `TAtscriptDataType<T>`.
542
+ *
543
+ * Use this for type-safe filters and selects with dot-notation field paths.
544
+ */
545
+ type FlatOf<T> = T extends {
546
+ __flat: infer F;
547
+ } ? F : TAtscriptDataType<T>;
548
+
538
549
  export { SERIALIZE_VERSION, Validator, ValidatorError, annotate, buildJsonSchema, cloneRefProp, createDataFromAnnotatedType, defineAnnotatedType, deserializeAnnotatedType, flattenAnnotatedType, forAnnotatedType, fromJsonSchema, isAnnotatedType, isAnnotatedTypeOfPrimitive, isPhantomType, mergeJsonSchemas, serializeAnnotatedType, throwFeatureDisabled };
539
- export type { InferDataType, TAnnotatedTypeHandle, TAtscriptAnnotatedType, TAtscriptAnnotatedTypeConstructor, TAtscriptDataType, TAtscriptTypeArray, TAtscriptTypeComplex, TAtscriptTypeDef, TAtscriptTypeFinal, TAtscriptTypeObject, TCreateDataOptions, TFlattenOptions, TJsonSchema, TMetadataMap, TProcessAnnotationContext, TSerializeOptions, TSerializedAnnotatedType, TSerializedAnnotatedTypeInner, TSerializedTypeArray, TSerializedTypeComplex, TSerializedTypeDef, TSerializedTypeFinal, TSerializedTypeObject, TValidatorOptions, TValidatorPlugin, TValidatorPluginContext, TValueResolver };
550
+ export type { FlatOf, InferDataType, TAnnotatedTypeHandle, TAtscriptAnnotatedType, TAtscriptAnnotatedTypeConstructor, TAtscriptDataType$1 as TAtscriptDataType, TAtscriptTypeArray, TAtscriptTypeComplex, TAtscriptTypeDef, TAtscriptTypeFinal, TAtscriptTypeObject, TCreateDataOptions, TFlattenOptions, TJsonSchema, TMetadataMap, TProcessAnnotationContext, TSerializeOptions, TSerializedAnnotatedType, TSerializedAnnotatedTypeInner, TSerializedTypeArray, TSerializedTypeComplex, TSerializedTypeDef, TSerializedTypeFinal, TSerializedTypeObject, TValidatorOptions, TValidatorPlugin, TValidatorPluginContext, TValueResolver };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/typescript",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Atscript: typescript-gen support.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -64,7 +64,7 @@
64
64
  "vitest": "3.2.4"
65
65
  },
66
66
  "peerDependencies": {
67
- "@atscript/core": "^0.1.29"
67
+ "@atscript/core": "^0.1.31"
68
68
  },
69
69
  "build": [
70
70
  {},