@kurotako/ir 0.1.0 → 0.2.0

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/index.js CHANGED
@@ -55,11 +55,29 @@ var IndexTypeSchema = v.picklist([
55
55
  "brin",
56
56
  "spgist"
57
57
  ]);
58
- var FieldTypeSchema = v.variant("kind", [
59
- v.object({ kind: v.literal("scalar"), scalar: ScalarTypeSchema }),
60
- v.object({ kind: v.literal("enum"), ref: v.string() }),
61
- v.object({ kind: v.literal("unknown"), hint: v.optional(v.string()) })
62
- ]);
58
+ var FieldTypeSchema = v.lazy(
59
+ () => v.variant("kind", [
60
+ v.object({ kind: v.literal("scalar"), scalar: ScalarTypeSchema }),
61
+ v.object({ kind: v.literal("enum"), ref: v.string() }),
62
+ v.object({ kind: v.literal("unknown"), hint: v.optional(v.string()) }),
63
+ v.object({ kind: v.literal("ref"), ref: v.string() }),
64
+ v.object({
65
+ kind: v.literal("union"),
66
+ variants: v.array(FieldTypeSchema),
67
+ discriminator: v.optional(
68
+ v.object({
69
+ propertyName: v.string(),
70
+ mapping: v.optional(v.record(v.string(), v.string()))
71
+ })
72
+ )
73
+ })
74
+ ])
75
+ );
76
+ var TypeAliasSchema = v.object({
77
+ name: v.string(),
78
+ type: FieldTypeSchema,
79
+ doc: v.optional(v.string())
80
+ });
63
81
  var ConstraintsSchema = v.object({
64
82
  min: v.optional(v.number()),
65
83
  max: v.optional(v.number()),
@@ -140,7 +158,8 @@ var SourceIrSchema = v.object({
140
158
  parser: v.string(),
141
159
  parserVersion: v.optional(v.string()),
142
160
  entities: v.record(v.string(), EntitySchema),
143
- enums: v.record(v.string(), EnumDefSchema)
161
+ enums: v.record(v.string(), EnumDefSchema),
162
+ typeAliases: v.optional(v.record(v.string(), TypeAliasSchema))
144
163
  });
145
164
  var IrSchema = v.object({
146
165
  irVersion: v.string(),
@@ -151,7 +170,7 @@ var IrSchema = v.object({
151
170
  import * as v2 from "valibot";
152
171
 
153
172
  // src/version.ts
154
- var IR_VERSION = "1";
173
+ var IR_VERSION = "2";
155
174
  function isCompatible(irVersion) {
156
175
  return irVersion === IR_VERSION;
157
176
  }
@@ -220,7 +239,134 @@ function checkEnumValues(issues, path, values) {
220
239
  seen.add(value.name);
221
240
  }
222
241
  }
223
- function checkSource(namespace, source, lookupEntity, isNamespacePresent, issues) {
242
+ function walkFieldType(type, path, entity, source, issues, info) {
243
+ switch (type.kind) {
244
+ case "scalar":
245
+ case "unknown":
246
+ return;
247
+ case "enum": {
248
+ const resolved = entity?.enums?.[type.ref] ?? source.enums[type.ref];
249
+ if (resolved === void 0) {
250
+ pushIssue(
251
+ issues,
252
+ path,
253
+ "unresolved_enum_ref",
254
+ `field type references unknown enum '${type.ref}'`
255
+ );
256
+ }
257
+ return;
258
+ }
259
+ case "ref": {
260
+ const known = source.entities[type.ref] !== void 0 || source.typeAliases?.[type.ref] !== void 0;
261
+ if (!known) {
262
+ pushIssue(
263
+ issues,
264
+ path,
265
+ "unresolved_ref",
266
+ `ref '${type.ref}' resolves to no entity and no type alias`
267
+ );
268
+ }
269
+ return;
270
+ }
271
+ case "union": {
272
+ if (type.variants.length < 2) {
273
+ pushIssue(
274
+ info,
275
+ path,
276
+ "degenerate_union",
277
+ `union has ${type.variants.length} variant(s); expected at least 2`
278
+ );
279
+ }
280
+ type.variants.forEach((variant2, i) => {
281
+ walkFieldType(
282
+ variant2,
283
+ `${path}.variants.${i}`,
284
+ entity,
285
+ source,
286
+ issues,
287
+ info
288
+ );
289
+ });
290
+ const mapping = type.discriminator?.mapping;
291
+ if (mapping !== void 0) {
292
+ const refVariants = new Set(
293
+ type.variants.flatMap((vv) => vv.kind === "ref" ? [vv.ref] : [])
294
+ );
295
+ for (const [key, target] of Object.entries(mapping)) {
296
+ if (!refVariants.has(target)) {
297
+ pushIssue(
298
+ issues,
299
+ `${path}.discriminator.mapping.${key}`,
300
+ "unresolved_type_alias",
301
+ `discriminator mapping '${key}' -> '${target}' names no ref variant of the union`
302
+ );
303
+ }
304
+ }
305
+ }
306
+ return;
307
+ }
308
+ }
309
+ }
310
+ function collectRefs(type, out) {
311
+ if (type.kind === "ref") {
312
+ out.add(type.ref);
313
+ } else if (type.kind === "union") {
314
+ for (const variant2 of type.variants) {
315
+ collectRefs(variant2, out);
316
+ }
317
+ }
318
+ }
319
+ function checkRefCycles(namespace, source, info) {
320
+ const adjacency = /* @__PURE__ */ new Map();
321
+ for (const [key, alias] of Object.entries(source.typeAliases ?? {})) {
322
+ const refs = /* @__PURE__ */ new Set();
323
+ collectRefs(alias.type, refs);
324
+ adjacency.set(key, refs);
325
+ }
326
+ for (const [key, entity] of Object.entries(source.entities)) {
327
+ const refs = adjacency.get(key) ?? /* @__PURE__ */ new Set();
328
+ for (const field of entity.fields) {
329
+ collectRefs(field.type, refs);
330
+ }
331
+ adjacency.set(key, refs);
332
+ }
333
+ const state = /* @__PURE__ */ new Map();
334
+ const stack = [];
335
+ const reported = /* @__PURE__ */ new Set();
336
+ const visit = (node) => {
337
+ state.set(node, "gray");
338
+ stack.push(node);
339
+ for (const next of adjacency.get(node) ?? []) {
340
+ if (!adjacency.has(next)) {
341
+ continue;
342
+ }
343
+ const seen = state.get(next);
344
+ if (seen === "gray") {
345
+ const cycle = stack.slice(stack.indexOf(next));
346
+ const signature = [...cycle].sort().join("|");
347
+ if (!reported.has(signature)) {
348
+ reported.add(signature);
349
+ pushIssue(
350
+ info,
351
+ namespace,
352
+ "union_cycle",
353
+ `reference cycle: ${[...cycle, next].join(" -> ")}`
354
+ );
355
+ }
356
+ } else if (seen === void 0) {
357
+ visit(next);
358
+ }
359
+ }
360
+ stack.pop();
361
+ state.set(node, "black");
362
+ };
363
+ for (const node of adjacency.keys()) {
364
+ if (state.get(node) === void 0) {
365
+ visit(node);
366
+ }
367
+ }
368
+ }
369
+ function checkSource(namespace, source, lookupEntity, isNamespacePresent, issues, info) {
224
370
  for (const [key, def] of Object.entries(source.enums)) {
225
371
  if (def.name !== key) {
226
372
  pushIssue(
@@ -255,17 +401,7 @@ function checkSource(namespace, source, lookupEntity, isNamespacePresent, issues
255
401
  }
256
402
  fieldNames.add(field.name);
257
403
  checkConstraints(issues, `${fPath}.constraints`, field.constraints);
258
- if (field.type.kind === "enum") {
259
- const resolved = entity.enums?.[field.type.ref] ?? source.enums[field.type.ref];
260
- if (resolved === void 0) {
261
- pushIssue(
262
- issues,
263
- fPath,
264
- "unresolved_enum_ref",
265
- `field '${field.name}' references unknown enum '${field.type.ref}'`
266
- );
267
- }
268
- }
404
+ walkFieldType(field.type, fPath, entity, source, issues, info);
269
405
  }
270
406
  for (const [localKey, def] of Object.entries(entity.enums ?? {})) {
271
407
  if (def.name !== localKey) {
@@ -363,6 +499,19 @@ function checkSource(namespace, source, lookupEntity, isNamespacePresent, issues
363
499
  }
364
500
  });
365
501
  }
502
+ for (const [key, alias] of Object.entries(source.typeAliases ?? {})) {
503
+ const aPath = `${namespace}.typeAliases.${key}`;
504
+ if (alias.name !== key) {
505
+ pushIssue(
506
+ issues,
507
+ aPath,
508
+ "type_alias_key_mismatch",
509
+ `type alias key '${key}' does not match name '${alias.name}'`
510
+ );
511
+ }
512
+ walkFieldType(alias.type, `${aPath}.type`, void 0, source, issues, info);
513
+ }
514
+ checkRefCycles(namespace, source, info);
366
515
  }
367
516
  function validateSourceIR(value) {
368
517
  const parsed = v2.safeParse(SourceIrSchema, value);
@@ -371,10 +520,14 @@ function validateSourceIR(value) {
371
520
  }
372
521
  const source = parsed.output;
373
522
  const issues = [];
523
+ const info = [];
374
524
  const lookup = (ns, name) => ns === source.namespace ? source.entities[name] : void 0;
375
525
  const isPresent = (ns) => ns === source.namespace;
376
- checkSource(source.namespace, source, lookup, isPresent, issues);
377
- return issues.length > 0 ? { ok: false, issues } : { ok: true, value: source };
526
+ checkSource(source.namespace, source, lookup, isPresent, issues, info);
527
+ if (issues.length > 0) {
528
+ return { ok: false, issues };
529
+ }
530
+ return info.length > 0 ? { ok: true, value: source, info } : { ok: true, value: source };
378
531
  }
379
532
  function validateIR(value) {
380
533
  const parsed = v2.safeParse(IrSchema, value);
@@ -383,6 +536,7 @@ function validateIR(value) {
383
536
  }
384
537
  const ir = parsed.output;
385
538
  const issues = [];
539
+ const info = [];
386
540
  if (!isCompatible(ir.irVersion)) {
387
541
  pushIssue(
388
542
  issues,
@@ -402,9 +556,12 @@ function validateIR(value) {
402
556
  `source key '${key}' does not match namespace '${source.namespace}'`
403
557
  );
404
558
  }
405
- checkSource(source.namespace, source, lookup, isPresent, issues);
559
+ checkSource(source.namespace, source, lookup, isPresent, issues, info);
560
+ }
561
+ if (issues.length > 0) {
562
+ return { ok: false, issues };
406
563
  }
407
- return issues.length > 0 ? { ok: false, issues } : { ok: true, value: ir };
564
+ return info.length > 0 ? { ok: true, value: ir, info } : { ok: true, value: ir };
408
565
  }
409
566
  function assertIR(value) {
410
567
  const result = validateIR(value);
@@ -463,6 +620,123 @@ var EnumBuilderImpl = class {
463
620
  return this.#def;
464
621
  }
465
622
  };
623
+ function checkedScalar(path, t) {
624
+ if (!v3.is(ScalarTypeSchema, t)) {
625
+ throw new IrBuildError(path, `unknown scalar type '${t}'`);
626
+ }
627
+ return { kind: "scalar", scalar: t };
628
+ }
629
+ var UnionBuilderImpl = class _UnionBuilderImpl {
630
+ #path;
631
+ #variants = [];
632
+ #discriminator;
633
+ constructor(path) {
634
+ this.#path = path;
635
+ }
636
+ scalar(t) {
637
+ this.#variants.push(checkedScalar(this.#path, t));
638
+ return this;
639
+ }
640
+ enum(ref) {
641
+ this.#variants.push({ kind: "enum", ref });
642
+ return this;
643
+ }
644
+ ref(name) {
645
+ this.#variants.push({ kind: "ref", ref: name });
646
+ return this;
647
+ }
648
+ union(build) {
649
+ const nested = new _UnionBuilderImpl(this.#path);
650
+ build(nested);
651
+ for (const variant2 of nested.#variants) {
652
+ this.#variants.push(variant2);
653
+ }
654
+ return this;
655
+ }
656
+ unknown(hint) {
657
+ this.#variants.push(
658
+ hint === void 0 ? { kind: "unknown" } : { kind: "unknown", hint }
659
+ );
660
+ return this;
661
+ }
662
+ discriminator(propertyName, mapping) {
663
+ this.#discriminator = mapping === void 0 ? { propertyName } : { propertyName, mapping };
664
+ return this;
665
+ }
666
+ build() {
667
+ if (this.#variants.length < 2) {
668
+ throw new IrBuildError(
669
+ this.#path,
670
+ `union() needs at least 2 variants, got ${this.#variants.length}`
671
+ );
672
+ }
673
+ const mapping = this.#discriminator?.mapping;
674
+ if (mapping !== void 0) {
675
+ const refVariants = new Set(
676
+ this.#variants.flatMap((vv) => vv.kind === "ref" ? [vv.ref] : [])
677
+ );
678
+ for (const [key, target] of Object.entries(mapping)) {
679
+ if (!refVariants.has(target)) {
680
+ throw new IrBuildError(
681
+ this.#path,
682
+ `discriminator mapping '${key}' -> '${target}' names no ref variant`
683
+ );
684
+ }
685
+ }
686
+ }
687
+ const type = {
688
+ kind: "union",
689
+ variants: this.#variants
690
+ };
691
+ if (this.#discriminator !== void 0) {
692
+ type.discriminator = this.#discriminator;
693
+ }
694
+ return type;
695
+ }
696
+ };
697
+ var TypeAliasBuilderImpl = class {
698
+ #path;
699
+ #name;
700
+ #type = { kind: "unknown" };
701
+ #doc;
702
+ constructor(path, name) {
703
+ this.#path = path;
704
+ this.#name = name;
705
+ }
706
+ scalar(t) {
707
+ this.#type = checkedScalar(this.#path, t);
708
+ return this;
709
+ }
710
+ enum(ref) {
711
+ this.#type = { kind: "enum", ref };
712
+ return this;
713
+ }
714
+ ref(name) {
715
+ this.#type = { kind: "ref", ref: name };
716
+ return this;
717
+ }
718
+ union(build) {
719
+ const nested = new UnionBuilderImpl(this.#path);
720
+ build(nested);
721
+ this.#type = nested.build();
722
+ return this;
723
+ }
724
+ unknown(hint) {
725
+ this.#type = hint === void 0 ? { kind: "unknown" } : { kind: "unknown", hint };
726
+ return this;
727
+ }
728
+ doc(text) {
729
+ this.#doc = text;
730
+ return this;
731
+ }
732
+ build() {
733
+ const alias = { name: this.#name, type: this.#type };
734
+ if (this.#doc !== void 0) {
735
+ alias.doc = this.#doc;
736
+ }
737
+ return alias;
738
+ }
739
+ };
466
740
  var FieldBuilderImpl = class {
467
741
  #path;
468
742
  #field;
@@ -490,6 +764,16 @@ var FieldBuilderImpl = class {
490
764
  this.#field.type = { kind: "enum", ref };
491
765
  return this;
492
766
  }
767
+ ref(name) {
768
+ this.#field.type = { kind: "ref", ref: name };
769
+ return this;
770
+ }
771
+ union(build) {
772
+ const builder = new UnionBuilderImpl(this.#path);
773
+ build(builder);
774
+ this.#field.type = builder.build();
775
+ return this;
776
+ }
493
777
  unknown(hint) {
494
778
  this.#field.type = hint === void 0 ? { kind: "unknown" } : { kind: "unknown", hint };
495
779
  return this;
@@ -734,6 +1018,7 @@ var SourceIrBuilderImpl = class {
734
1018
  #entities = [];
735
1019
  #entityNames = /* @__PURE__ */ new Set();
736
1020
  #enums = {};
1021
+ #typeAliases = {};
737
1022
  constructor(init) {
738
1023
  this.#namespace = init.namespace;
739
1024
  this.#parser = init.parser;
@@ -758,6 +1043,21 @@ var SourceIrBuilderImpl = class {
758
1043
  this.#entities.push(builder);
759
1044
  return this;
760
1045
  }
1046
+ addTypeAlias(name, def) {
1047
+ if (name in this.#typeAliases) {
1048
+ throw new IrBuildError(
1049
+ `${this.#namespace}.typeAliases.${name}`,
1050
+ `duplicate type alias '${name}'`
1051
+ );
1052
+ }
1053
+ const builder = new TypeAliasBuilderImpl(
1054
+ `${this.#namespace}.typeAliases.${name}`,
1055
+ name
1056
+ );
1057
+ def(builder);
1058
+ this.#typeAliases[name] = builder.build();
1059
+ return this;
1060
+ }
761
1061
  build() {
762
1062
  const source = {
763
1063
  namespace: this.#namespace,
@@ -773,6 +1073,9 @@ var SourceIrBuilderImpl = class {
773
1073
  if (this.#parserVersion !== void 0) {
774
1074
  source.parserVersion = this.#parserVersion;
775
1075
  }
1076
+ if (Object.keys(this.#typeAliases).length > 0) {
1077
+ source.typeAliases = this.#typeAliases;
1078
+ }
776
1079
  try {
777
1080
  assertSourceIR(source);
778
1081
  } catch (err) {
@@ -803,6 +1106,112 @@ function resolveEntity(ir, namespace, name) {
803
1106
  function resolveEnum(source, entity, ref) {
804
1107
  return entity?.enums?.[ref] ?? source.enums[ref];
805
1108
  }
1109
+ function resolveRef(source, ref) {
1110
+ return source.entities[ref] ?? source.typeAliases?.[ref];
1111
+ }
1112
+ function resolveTypeAlias(source, name) {
1113
+ return source.typeAliases?.[name];
1114
+ }
1115
+ function* iterTypeAliases(ir) {
1116
+ for (const [namespace, source] of Object.entries(ir.sources)) {
1117
+ for (const alias of Object.values(source.typeAliases ?? {})) {
1118
+ yield { namespace, alias };
1119
+ }
1120
+ }
1121
+ }
1122
+ function collectRefNames(type, into = /* @__PURE__ */ new Set()) {
1123
+ if (type.kind === "ref") {
1124
+ into.add(type.ref);
1125
+ } else if (type.kind === "union") {
1126
+ for (const variant2 of type.variants) {
1127
+ collectRefNames(variant2, into);
1128
+ }
1129
+ }
1130
+ return into;
1131
+ }
1132
+ function refCycleMembers(source) {
1133
+ const adjacency = /* @__PURE__ */ new Map();
1134
+ const edgesFor = (key) => {
1135
+ let set = adjacency.get(key);
1136
+ if (set === void 0) {
1137
+ set = /* @__PURE__ */ new Set();
1138
+ adjacency.set(key, set);
1139
+ }
1140
+ return set;
1141
+ };
1142
+ for (const [key, alias] of Object.entries(source.typeAliases ?? {})) {
1143
+ collectRefNames(alias.type, edgesFor(key));
1144
+ }
1145
+ for (const [key, entity] of Object.entries(source.entities)) {
1146
+ const set = edgesFor(key);
1147
+ for (const field of entity.fields) {
1148
+ collectRefNames(field.type, set);
1149
+ }
1150
+ }
1151
+ const canReachSelf = (start) => {
1152
+ const seen = /* @__PURE__ */ new Set();
1153
+ const stack = [...adjacency.get(start) ?? []];
1154
+ while (stack.length > 0) {
1155
+ const node = stack.pop();
1156
+ if (node === void 0) {
1157
+ break;
1158
+ }
1159
+ if (node === start) {
1160
+ return true;
1161
+ }
1162
+ if (seen.has(node) || !adjacency.has(node)) {
1163
+ continue;
1164
+ }
1165
+ seen.add(node);
1166
+ for (const next of adjacency.get(node) ?? []) {
1167
+ stack.push(next);
1168
+ }
1169
+ }
1170
+ return false;
1171
+ };
1172
+ const members = /* @__PURE__ */ new Set();
1173
+ for (const node of adjacency.keys()) {
1174
+ if (canReachSelf(node)) {
1175
+ members.add(node);
1176
+ }
1177
+ }
1178
+ return members;
1179
+ }
1180
+ function fieldTypeKey(type) {
1181
+ switch (type.kind) {
1182
+ case "scalar":
1183
+ return `scalar:${type.scalar}`;
1184
+ case "enum":
1185
+ return `enum:${type.ref}`;
1186
+ case "ref":
1187
+ return `ref:${type.ref}`;
1188
+ case "unknown":
1189
+ return `unknown:${type.hint ?? ""}`;
1190
+ case "union":
1191
+ return `union:${flattenUnion(type).map(fieldTypeKey).join(",")}`;
1192
+ }
1193
+ }
1194
+ function flattenUnion(type) {
1195
+ const out = [];
1196
+ const seen = /* @__PURE__ */ new Set();
1197
+ const push = (t) => {
1198
+ if (t.kind === "union") {
1199
+ for (const variant2 of t.variants) {
1200
+ push(variant2);
1201
+ }
1202
+ return;
1203
+ }
1204
+ const key = fieldTypeKey(t);
1205
+ if (!seen.has(key)) {
1206
+ seen.add(key);
1207
+ out.push(t);
1208
+ }
1209
+ };
1210
+ for (const variant2 of type.variants) {
1211
+ push(variant2);
1212
+ }
1213
+ return out;
1214
+ }
806
1215
  function isCrossSource(fromNamespace, rel) {
807
1216
  return rel.target.namespace !== fromNamespace;
808
1217
  }
@@ -855,10 +1264,16 @@ function scalarTsType(type) {
855
1264
  switch (type.kind) {
856
1265
  case "enum":
857
1266
  return type.ref;
1267
+ case "ref":
1268
+ return type.ref;
858
1269
  case "unknown":
859
1270
  return "unknown";
860
1271
  case "scalar":
861
1272
  return mapScalar(type.scalar);
1273
+ case "union":
1274
+ return flattenUnion(type).map(
1275
+ (variant2) => variant2.kind === "union" ? `(${scalarTsType(variant2)})` : scalarTsType(variant2)
1276
+ ).join(" | ");
862
1277
  }
863
1278
  }
864
1279
  function mapScalar(scalar) {
@@ -905,10 +1320,13 @@ export {
905
1320
  ScalarTypeSchema,
906
1321
  SourceIrSchema,
907
1322
  StringFormatSchema,
1323
+ TypeAliasSchema,
908
1324
  assertIR,
909
1325
  assertSourceIR,
1326
+ collectRefNames,
910
1327
  createFields,
911
1328
  createSourceIR,
1329
+ flattenUnion,
912
1330
  getSource,
913
1331
  isCompatible,
914
1332
  isCreateOptional,
@@ -916,11 +1334,15 @@ export {
916
1334
  isDbAssigned,
917
1335
  iterEntities,
918
1336
  iterFields,
1337
+ iterTypeAliases,
919
1338
  parseIR,
920
1339
  primaryKeyFields,
1340
+ refCycleMembers,
921
1341
  resolveEntity,
922
1342
  resolveEnum,
1343
+ resolveRef,
923
1344
  resolveRelationTarget,
1345
+ resolveTypeAlias,
924
1346
  scalarTsType,
925
1347
  updateFields,
926
1348
  validateIR,