@palantir/pack.schema 0.30.0 → 0.32.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/.turbo/turbo-lint.log +3 -3
- package/.turbo/turbo-transpileBrowser.log +1 -1
- package/.turbo/turbo-transpileCjs.log +1 -1
- package/.turbo/turbo-transpileEsm.log +1 -1
- package/.turbo/turbo-transpileTypes.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +13 -0
- package/build/browser/index.js +36 -8
- package/build/browser/index.js.map +1 -1
- package/build/cjs/index.cjs +37 -7
- package/build/cjs/index.cjs.map +1 -1
- package/build/cjs/index.d.cts +14 -3
- package/build/esm/index.js +36 -8
- package/build/esm/index.js.map +1 -1
- package/build/types/defineMigration.d.ts +2 -2
- package/build/types/defineMigration.d.ts.map +1 -1
- package/build/types/defineUnion.d.ts +11 -0
- package/build/types/defineUnion.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/defineSchemaUpdate.test.ts +157 -0
- package/src/__tests__/defineUnion.test.ts +20 -0
- package/src/defineMigration.ts +20 -10
- package/src/defineUnion.ts +40 -3
|
@@ -43,6 +43,7 @@ describe("defineSchemaUpdate", () => {
|
|
|
43
43
|
const schemaV2 = nextSchema(schemaV1).addSchemaUpdate(addAge).build();
|
|
44
44
|
|
|
45
45
|
expect(schemaV2.models.Person).toBe(schemaV1.models.Person);
|
|
46
|
+
expect(schemaV2.models.PersonV2.docs).toBe("A person");
|
|
46
47
|
expect(schemaV2.models.PersonV2.fields.name).toEqual({ type: "string" });
|
|
47
48
|
expect(schemaV2.models.PersonV2.fields.age).toEqual({ type: "double" });
|
|
48
49
|
expect(schemaV2.version).toBe(2);
|
|
@@ -53,6 +54,50 @@ describe("defineSchemaUpdate", () => {
|
|
|
53
54
|
assertTypeEquals<typeof schemaV2.models.PersonV2.fields.age, P.Double>();
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
it("should preserve record documentation through field additions and deprecations", () => {
|
|
58
|
+
const schemaV1 = defineSchema({
|
|
59
|
+
Person: defineRecord("Person", {
|
|
60
|
+
docs: "A person",
|
|
61
|
+
fields: { name: P.String },
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const schemaV2 = nextSchema(schemaV1)
|
|
66
|
+
.addSchemaUpdate(defineSchemaUpdate("addDisplayName", schema => ({
|
|
67
|
+
Person: schema.Person
|
|
68
|
+
.addField("displayName", P.String, { derivedFrom: ["name"] })
|
|
69
|
+
.deprecateField("name", "Use displayName instead.")
|
|
70
|
+
.build(),
|
|
71
|
+
})))
|
|
72
|
+
.build();
|
|
73
|
+
|
|
74
|
+
expect(schemaV2.models.Person.docs).toBe("A person");
|
|
75
|
+
expect(schemaV2.models.Person.fields).toEqual({ name: P.String, displayName: P.String });
|
|
76
|
+
expect(schemaV2.migrations?.Person?.displayName).toEqual({ derivedFrom: ["name"] });
|
|
77
|
+
expect(schemaV2.deprecations?.Person?.name).toEqual({
|
|
78
|
+
fromVersion: 2,
|
|
79
|
+
message: "Use displayName instead.",
|
|
80
|
+
});
|
|
81
|
+
expect(schemaV1.models.Person.fields).toEqual({ name: P.String });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should preserve a record when building without changing fields", () => {
|
|
85
|
+
const schemaV1 = defineSchema({
|
|
86
|
+
Person: defineRecord("Person", {
|
|
87
|
+
docs: "A person",
|
|
88
|
+
fields: { name: P.String },
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const schemaV2 = nextSchema(schemaV1)
|
|
93
|
+
.addSchemaUpdate(defineSchemaUpdate("rebuildPerson", schema => ({
|
|
94
|
+
Person: schema.Person.build(),
|
|
95
|
+
})))
|
|
96
|
+
.build();
|
|
97
|
+
|
|
98
|
+
expect(schemaV2.models.Person).toEqual(schemaV1.models.Person);
|
|
99
|
+
});
|
|
100
|
+
|
|
56
101
|
it("should compose multiple schema updates in one version", () => {
|
|
57
102
|
const schemaV1 = defineSchema({
|
|
58
103
|
ShapeBox: defineRecord("ShapeBox", {
|
|
@@ -153,6 +198,7 @@ describe("defineSchemaUpdate", () => {
|
|
|
153
198
|
const schemaV3 = nextSchema(schemaV2).addSchemaUpdate(addTags).build();
|
|
154
199
|
|
|
155
200
|
expect(schemaV3.version).toBe(3);
|
|
201
|
+
expect(schemaV3.models.Item.docs).toBe("An item");
|
|
156
202
|
expect(schemaV3.models.Item.fields.name).toEqual({ type: "string" });
|
|
157
203
|
expect(schemaV3.models.Item.fields.description).toEqual({
|
|
158
204
|
type: "optional",
|
|
@@ -287,6 +333,8 @@ describe("defineSchemaUpdate", () => {
|
|
|
287
333
|
|
|
288
334
|
const schemaV2 = nextSchema(schemaV1).addSchemaUpdate(addTriangle).build();
|
|
289
335
|
|
|
336
|
+
expect(schemaV2.models.Shape.discriminant).toBe("type");
|
|
337
|
+
expect(schemaV2.models.Shape.docs).toBe("A shape");
|
|
290
338
|
expect(schemaV2.models.Shape.variants.circle).toEqual({
|
|
291
339
|
type: "ref",
|
|
292
340
|
name: "Circle",
|
|
@@ -305,6 +353,115 @@ describe("defineSchemaUpdate", () => {
|
|
|
305
353
|
>();
|
|
306
354
|
});
|
|
307
355
|
|
|
356
|
+
it("should preserve union metadata through chained variant additions", () => {
|
|
357
|
+
const Circle = defineRecord("Circle", {
|
|
358
|
+
docs: "A circle",
|
|
359
|
+
fields: { radius: P.Double },
|
|
360
|
+
});
|
|
361
|
+
const Rectangle = defineRecord("Rectangle", {
|
|
362
|
+
docs: "A rectangle",
|
|
363
|
+
fields: { width: P.Double, height: P.Double },
|
|
364
|
+
});
|
|
365
|
+
const Triangle = defineRecord("Triangle", {
|
|
366
|
+
docs: "A triangle",
|
|
367
|
+
fields: { base: P.Double, height: P.Double },
|
|
368
|
+
});
|
|
369
|
+
const schemaV1 = defineSchema({
|
|
370
|
+
Circle,
|
|
371
|
+
Rectangle,
|
|
372
|
+
Triangle,
|
|
373
|
+
Shape: defineUnion("Shape", {
|
|
374
|
+
discriminant: "kind",
|
|
375
|
+
docs: "A shape",
|
|
376
|
+
variants: { circle: Circle },
|
|
377
|
+
}),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const schemaV2 = nextSchema(schemaV1)
|
|
381
|
+
.addSchemaUpdate(defineSchemaUpdate("addShapes", schema => ({
|
|
382
|
+
Shape: schema.Shape.addVariant("rectangle", Rectangle)
|
|
383
|
+
.addVariant("triangle", Triangle)
|
|
384
|
+
.build(),
|
|
385
|
+
})))
|
|
386
|
+
.build();
|
|
387
|
+
|
|
388
|
+
expect(schemaV2.models.Shape).toEqual({
|
|
389
|
+
...schemaV1.models.Shape,
|
|
390
|
+
variants: {
|
|
391
|
+
circle: { type: "ref", name: "Circle", refType: "record" },
|
|
392
|
+
rectangle: { type: "ref", name: "Rectangle", refType: "record" },
|
|
393
|
+
triangle: { type: "ref", name: "Triangle", refType: "record" },
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
expect(schemaV1.models.Shape.variants).toEqual({
|
|
397
|
+
circle: { type: "ref", name: "Circle", refType: "record" },
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const schemaV3 = nextSchema(schemaV2)
|
|
401
|
+
.addSchemaUpdate(defineSchemaUpdate("rebuildShape", schema => ({
|
|
402
|
+
Shape: schema.Shape.build(),
|
|
403
|
+
})))
|
|
404
|
+
.build();
|
|
405
|
+
|
|
406
|
+
expect(schemaV3.models.Shape).toEqual(schemaV2.models.Shape);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should preserve a union when building without adding variants", () => {
|
|
410
|
+
const Circle = defineRecord("Circle", {
|
|
411
|
+
docs: "A circle",
|
|
412
|
+
fields: { radius: P.Double },
|
|
413
|
+
});
|
|
414
|
+
const schemaV1 = defineSchema({
|
|
415
|
+
Circle,
|
|
416
|
+
Shape: defineUnion("Shape", {
|
|
417
|
+
discriminant: "kind",
|
|
418
|
+
docs: "A shape",
|
|
419
|
+
variants: { circle: Circle },
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const schemaV2 = nextSchema(schemaV1)
|
|
424
|
+
.addSchemaUpdate(defineSchemaUpdate("rebuildShape", schema => ({
|
|
425
|
+
Shape: schema.Shape.build(),
|
|
426
|
+
})))
|
|
427
|
+
.build();
|
|
428
|
+
|
|
429
|
+
expect(schemaV2.models.Shape).toEqual(schemaV1.models.Shape);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it.each(["circle", "rectangle"] as const)("should reject duplicate variant %s", variantName => {
|
|
433
|
+
const Circle = defineRecord("Circle", {
|
|
434
|
+
docs: "A circle",
|
|
435
|
+
fields: { radius: P.Double },
|
|
436
|
+
});
|
|
437
|
+
const Rectangle = defineRecord("Rectangle", {
|
|
438
|
+
docs: "A rectangle",
|
|
439
|
+
fields: { width: P.Double, height: P.Double },
|
|
440
|
+
});
|
|
441
|
+
const schemaV1 = defineSchema({
|
|
442
|
+
Circle,
|
|
443
|
+
Rectangle,
|
|
444
|
+
Shape: defineUnion("Shape", {
|
|
445
|
+
docs: "A shape",
|
|
446
|
+
variants: { circle: Circle },
|
|
447
|
+
}),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(() =>
|
|
451
|
+
nextSchema(schemaV1)
|
|
452
|
+
.addSchemaUpdate(defineSchemaUpdate("addShapes", schema => ({
|
|
453
|
+
Shape: schema.Shape.addVariant("rectangle", Rectangle)
|
|
454
|
+
// @ts-expect-error Existing variant names cannot be added again.
|
|
455
|
+
.addVariant(variantName, Rectangle)
|
|
456
|
+
.build(),
|
|
457
|
+
})))
|
|
458
|
+
.build()
|
|
459
|
+
).toThrow(`Variant "${variantName}" already exists in union "Shape".`);
|
|
460
|
+
expect(schemaV1.models.Shape.variants).toEqual({
|
|
461
|
+
circle: { type: "ref", name: "Circle", refType: "record" },
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
308
465
|
it("should handle mixed record and union updates in one version", () => {
|
|
309
466
|
const PersonRecord = defineRecord("Person", {
|
|
310
467
|
docs: "A person",
|
|
@@ -268,3 +268,23 @@ describe("defineUnion", () => {
|
|
|
268
268
|
});
|
|
269
269
|
});
|
|
270
270
|
});
|
|
271
|
+
|
|
272
|
+
describe("reserved union variant names", () => {
|
|
273
|
+
const Circle = defineRecord("Circle", { docs: "A circle", fields: { radius: P.Double } });
|
|
274
|
+
|
|
275
|
+
it.each(["unknown", "Unknown", "_unknown"])("should reject a variant named %s", variantName => {
|
|
276
|
+
expect(() =>
|
|
277
|
+
defineUnion("Shape", { docs: "A shape", variants: { circle: Circle, [variantName]: Circle } })
|
|
278
|
+
).toThrow(`Variant "${variantName}" in union "Shape" uses a reserved name.`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should reject rawValue as the discriminant", () => {
|
|
282
|
+
expect(() =>
|
|
283
|
+
defineUnion("Shape", {
|
|
284
|
+
discriminant: "rawValue",
|
|
285
|
+
docs: "A shape",
|
|
286
|
+
variants: { circle: Circle },
|
|
287
|
+
})
|
|
288
|
+
).toThrow("Union \"Shape\" can't use \"rawValue\" as its discriminant.");
|
|
289
|
+
});
|
|
290
|
+
});
|
package/src/defineMigration.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { UnionVariantArg } from "./defineUnion.js";
|
|
18
|
-
import { resolveUnionVariantArg } from "./defineUnion.js";
|
|
18
|
+
import { assertVariantNameAllowed, resolveUnionVariantArg } from "./defineUnion.js";
|
|
19
19
|
import type { ModelDefs, RecordDef, RecordFields, UnionDef, UnionVariants } from "./defs.js";
|
|
20
20
|
import { ModelDefType } from "./defs.js";
|
|
21
21
|
import type { Ref, Type } from "./primitives.js";
|
|
@@ -83,9 +83,9 @@ export interface RecordBuilder<T extends Record<string, Type>> {
|
|
|
83
83
|
/** Evolves a union within a {@link SchemaUpdate}. Obtain one from the {@link SchemaBuilder} and finish with `build()`. */
|
|
84
84
|
export interface UnionBuilder<S extends UnionVariants> {
|
|
85
85
|
// TODO: builders should support arg types and resolveModels to refs
|
|
86
|
-
/** Add a variant, referencing a record (or a `Ref` to one). */
|
|
86
|
+
/** Add a new variant, referencing a record (or a `Ref` to one). */
|
|
87
87
|
addVariant<const K extends string>(
|
|
88
|
-
name: K,
|
|
88
|
+
name: string extends keyof S ? K : Exclude<K, keyof S>,
|
|
89
89
|
modelDefOrRef: UnionVariantArg,
|
|
90
90
|
): UnionBuilder<S & { [k in K]: Ref }>;
|
|
91
91
|
/** Produce the updated {@link UnionDef}. */
|
|
@@ -95,32 +95,40 @@ export interface UnionBuilder<S extends UnionVariants> {
|
|
|
95
95
|
class UnionBuilderImpl<S extends UnionVariants> implements UnionBuilder<S> {
|
|
96
96
|
private readonly name: string;
|
|
97
97
|
private readonly docs: string | undefined;
|
|
98
|
+
private readonly discriminant: string;
|
|
98
99
|
private readonly variants: S;
|
|
99
|
-
constructor({ name, docs, variants }: UnionDef<S>) {
|
|
100
|
+
constructor({ name, docs, discriminant, variants }: UnionDef<S>) {
|
|
100
101
|
this.name = name;
|
|
101
102
|
this.docs = docs;
|
|
103
|
+
this.discriminant = discriminant;
|
|
102
104
|
this.variants = { ...variants };
|
|
103
105
|
}
|
|
104
106
|
addVariant<const K extends string>(
|
|
105
|
-
variantName: K,
|
|
107
|
+
variantName: string extends keyof S ? K : Exclude<K, keyof S>,
|
|
106
108
|
modelDefOrRef: UnionVariantArg,
|
|
107
109
|
): UnionBuilder<S & Record<K, Ref>> {
|
|
110
|
+
if (Object.hasOwn(this.variants, variantName)) {
|
|
111
|
+
throw new Error(`Variant "${variantName}" already exists in union "${this.name}".`);
|
|
112
|
+
}
|
|
113
|
+
assertVariantNameAllowed(variantName, this.name);
|
|
108
114
|
const ref: Ref = resolveUnionVariantArg(modelDefOrRef);
|
|
109
115
|
const newVariants = { ...this.variants, [variantName]: ref };
|
|
110
116
|
|
|
111
117
|
return new UnionBuilderImpl({
|
|
112
118
|
type: ModelDefType.UNION,
|
|
113
119
|
name: this.name,
|
|
120
|
+
docs: this.docs,
|
|
114
121
|
variants: newVariants,
|
|
115
|
-
discriminant:
|
|
122
|
+
discriminant: this.discriminant,
|
|
116
123
|
});
|
|
117
124
|
}
|
|
118
125
|
build(): UnionDef<S> {
|
|
119
126
|
return {
|
|
120
127
|
type: ModelDefType.UNION,
|
|
121
128
|
variants: this.variants,
|
|
122
|
-
discriminant:
|
|
129
|
+
discriminant: this.discriminant,
|
|
123
130
|
name: this.name,
|
|
131
|
+
docs: this.docs,
|
|
124
132
|
};
|
|
125
133
|
}
|
|
126
134
|
}
|
|
@@ -149,6 +157,7 @@ export interface FieldDeprecation {
|
|
|
149
157
|
|
|
150
158
|
class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
|
|
151
159
|
private readonly name: string;
|
|
160
|
+
private readonly docs: string | undefined;
|
|
152
161
|
private readonly fields: T;
|
|
153
162
|
private readonly upgradeOptions: ReadonlyMap<string, FieldOptions>;
|
|
154
163
|
private readonly deprecatedFields: ReadonlyMap<string, FieldDeprecation>;
|
|
@@ -159,6 +168,7 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
|
|
|
159
168
|
deprecatedFields?: ReadonlyMap<string, FieldDeprecation>,
|
|
160
169
|
) {
|
|
161
170
|
this.name = initialRecordDef.name;
|
|
171
|
+
this.docs = initialRecordDef.docs;
|
|
162
172
|
this.fields = { ...initialRecordDef.fields };
|
|
163
173
|
// Upgrade options only flow forward within a single migration callback —
|
|
164
174
|
// they are NOT inherited from the prior version's RecordDef, because each
|
|
@@ -182,7 +192,7 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
|
|
|
182
192
|
type: ModelDefType.RECORD,
|
|
183
193
|
name: this.name,
|
|
184
194
|
fields: { ...this.fields, [name]: value },
|
|
185
|
-
docs:
|
|
195
|
+
docs: this.docs,
|
|
186
196
|
},
|
|
187
197
|
next,
|
|
188
198
|
this.deprecatedFields,
|
|
@@ -200,7 +210,7 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
|
|
|
200
210
|
type: ModelDefType.RECORD,
|
|
201
211
|
name: this.name,
|
|
202
212
|
fields: { ...this.fields },
|
|
203
|
-
docs:
|
|
213
|
+
docs: this.docs,
|
|
204
214
|
},
|
|
205
215
|
this.upgradeOptions,
|
|
206
216
|
next,
|
|
@@ -212,7 +222,7 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
|
|
|
212
222
|
type: ModelDefType.RECORD,
|
|
213
223
|
name: this.name,
|
|
214
224
|
fields: this.fields,
|
|
215
|
-
docs:
|
|
225
|
+
docs: this.docs,
|
|
216
226
|
};
|
|
217
227
|
if (this.upgradeOptions.size > 0) {
|
|
218
228
|
Object.defineProperty(result, UPGRADE_OPTIONS_CARRIER, {
|
package/src/defineUnion.ts
CHANGED
|
@@ -61,9 +61,11 @@ export function defineUnion<const TVariants extends UnionVariantArgs>(
|
|
|
61
61
|
variants,
|
|
62
62
|
}: UnionArgs<TVariants>,
|
|
63
63
|
): UnionDef<ResolvedUnionVariants<TVariants>> {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
assertDiscriminantAllowed(discriminant, name);
|
|
65
|
+
const entries = Object.entries(variants).map(([key, value]) => {
|
|
66
|
+
assertVariantNameAllowed(key, name);
|
|
67
|
+
return [key, resolveUnionVariantArg(value)] as const;
|
|
68
|
+
});
|
|
67
69
|
|
|
68
70
|
const resolvedVariants = Object.fromEntries(entries) as ResolvedUnionVariants<TVariants>;
|
|
69
71
|
|
|
@@ -80,3 +82,38 @@ export function resolveUnionVariantArg(arg: UnionVariantArg): Ref {
|
|
|
80
82
|
const resolved = typeof arg === "function" ? arg() : arg;
|
|
81
83
|
return resolved.type === TypeKind.REF ? resolved : modelToRef(resolved);
|
|
82
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The name a variant gets in generated code: kebab-case or snake_case to PascalCase,
|
|
88
|
+
* e.g. "my-box" -> "MyBox", "_unknown" -> "Unknown".
|
|
89
|
+
*/
|
|
90
|
+
export function formatVariantName(variantName: string): string {
|
|
91
|
+
return variantName
|
|
92
|
+
.split(/[-_]/)
|
|
93
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
94
|
+
.join("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Throw if a variant uses a reserved name. Every generated union has an Unknown case for variants
|
|
99
|
+
* an SDK doesn't know: its discriminant is "unknown" and its generated names end in "Unknown", so
|
|
100
|
+
* no variant may produce either (e.g. "unknown", "Unknown", "_unknown").
|
|
101
|
+
*/
|
|
102
|
+
export function assertVariantNameAllowed(variantName: string, unionName: string): void {
|
|
103
|
+
if (formatVariantName(variantName) === "Unknown") {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Variant "${variantName}" in union "${unionName}" uses a reserved name. Every union has a`
|
|
106
|
+
+ " generated Unknown case for variants an SDK doesn't know; choose another name.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Throw if a discriminant uses `rawValue`, the field the Unknown case keeps the stored value in. */
|
|
112
|
+
function assertDiscriminantAllowed(discriminant: string, unionName: string): void {
|
|
113
|
+
if (discriminant === "rawValue") {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Union "${unionName}" can't use "rawValue" as its discriminant. The generated Unknown case`
|
|
116
|
+
+ " stores the original value in rawValue; choose another name.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|