@palantir/pack.schema 0.3.1 → 0.5.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.
@@ -0,0 +1,365 @@
1
+ /*
2
+ * Copyright 2026 Palantir Technologies, Inc. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { describe, expect, it } from "vitest";
18
+ import type { SchemaBuilder } from "../defineMigration.js";
19
+ import { defineRecord } from "../defineRecord.js";
20
+ import { defineSchema, defineSchemaUpdate, nextSchema } from "../defineSchemaUpdate.js";
21
+ import { defineUnion } from "../defineUnion.js";
22
+ import * as P from "../primitives.js";
23
+ import { assertExactKeys, assertHasKeys, assertTypeEquals } from "./testTypeUtils.js";
24
+
25
+ describe("defineSchemaUpdate", () => {
26
+ it("should create a schema update and apply it via nextSchema", () => {
27
+ const schemaV1 = defineSchema({
28
+ Person: defineRecord("Person", {
29
+ docs: "A person",
30
+ fields: {
31
+ name: P.String,
32
+ },
33
+ }),
34
+ });
35
+
36
+ const addAge = defineSchemaUpdate(
37
+ "addAge",
38
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
39
+ PersonV2: schema.Person.addField("age", P.Double).build(),
40
+ }),
41
+ );
42
+
43
+ const schemaV2 = nextSchema(schemaV1).addSchemaUpdate(addAge).build();
44
+
45
+ expect(schemaV2.models.Person).toBe(schemaV1.models.Person);
46
+ expect(schemaV2.models.PersonV2.fields.name).toEqual({ type: "string" });
47
+ expect(schemaV2.models.PersonV2.fields.age).toEqual({ type: "double" });
48
+ expect(schemaV2.version).toBe(2);
49
+
50
+ assertHasKeys<typeof schemaV2.models, "Person" | "PersonV2">();
51
+ assertExactKeys<typeof schemaV2.models.PersonV2.fields, "name" | "age">();
52
+ assertTypeEquals<typeof schemaV2.models.PersonV2.fields.name, P.String>();
53
+ assertTypeEquals<typeof schemaV2.models.PersonV2.fields.age, P.Double>();
54
+ });
55
+
56
+ it("should compose multiple schema updates in one version", () => {
57
+ const schemaV1 = defineSchema({
58
+ ShapeBox: defineRecord("ShapeBox", {
59
+ docs: "A box shape",
60
+ fields: {
61
+ left: P.Double,
62
+ right: P.Double,
63
+ color: P.Optional(P.String),
64
+ },
65
+ }),
66
+ ShapeCircle: defineRecord("ShapeCircle", {
67
+ docs: "A circle shape",
68
+ fields: {
69
+ cx: P.Double,
70
+ radius: P.Double,
71
+ color: P.Optional(P.String),
72
+ },
73
+ }),
74
+ });
75
+
76
+ const addFillColor = defineSchemaUpdate(
77
+ "addFillColor",
78
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
79
+ ShapeBox: schema.ShapeBox
80
+ .addField("fillColor", P.Optional(P.String))
81
+ .build(),
82
+ ShapeCircle: schema.ShapeCircle
83
+ .addField("fillColor", P.Optional(P.String))
84
+ .build(),
85
+ }),
86
+ );
87
+
88
+ const addOpacity = defineSchemaUpdate(
89
+ "addOpacity",
90
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
91
+ ShapeBox: schema.ShapeBox
92
+ .addField("opacity", P.Optional(P.Double))
93
+ .build(),
94
+ ShapeCircle: schema.ShapeCircle
95
+ .addField("opacity", P.Optional(P.Double))
96
+ .build(),
97
+ }),
98
+ );
99
+
100
+ const schemaV2 = nextSchema(schemaV1)
101
+ .addSchemaUpdate(addFillColor)
102
+ .addSchemaUpdate(addOpacity)
103
+ .build();
104
+
105
+ expect(schemaV2.models.ShapeBox.fields.fillColor).toEqual({
106
+ type: "optional",
107
+ item: { type: "string" },
108
+ });
109
+ expect(schemaV2.models.ShapeBox.fields.opacity).toEqual({
110
+ type: "optional",
111
+ item: { type: "double" },
112
+ });
113
+ expect(schemaV2.models.ShapeBox.fields.left).toEqual({ type: "double" });
114
+ expect(schemaV2.models.ShapeCircle.fields.fillColor).toEqual({
115
+ type: "optional",
116
+ item: { type: "string" },
117
+ });
118
+ expect(schemaV2.models.ShapeCircle.fields.opacity).toEqual({
119
+ type: "optional",
120
+ item: { type: "double" },
121
+ });
122
+ expect(schemaV2.version).toBe(2);
123
+ });
124
+
125
+ it("should track version advancement across multiple nextSchema calls", () => {
126
+ const schemaV1 = defineSchema({
127
+ Item: defineRecord("Item", {
128
+ docs: "An item",
129
+ fields: {
130
+ name: P.String,
131
+ },
132
+ }),
133
+ });
134
+
135
+ const addDescription = defineSchemaUpdate(
136
+ "addDescription",
137
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
138
+ Item: schema.Item.addField("description", P.Optional(P.String)).build(),
139
+ }),
140
+ );
141
+
142
+ const schemaV2 = nextSchema(schemaV1).addSchemaUpdate(addDescription).build();
143
+
144
+ expect(schemaV2.version).toBe(2);
145
+
146
+ const addTags = defineSchemaUpdate(
147
+ "addTags",
148
+ (schema: SchemaBuilder<typeof schemaV2.models>) => ({
149
+ Item: schema.Item.addField("tags", P.Optional(P.String)).build(),
150
+ }),
151
+ );
152
+
153
+ const schemaV3 = nextSchema(schemaV2).addSchemaUpdate(addTags).build();
154
+
155
+ expect(schemaV3.version).toBe(3);
156
+ expect(schemaV3.models.Item.fields.name).toEqual({ type: "string" });
157
+ expect(schemaV3.models.Item.fields.description).toEqual({
158
+ type: "optional",
159
+ item: { type: "string" },
160
+ });
161
+ expect(schemaV3.models.Item.fields.tags).toEqual({
162
+ type: "optional",
163
+ item: { type: "string" },
164
+ });
165
+ });
166
+
167
+ it("should build a walkable version chain via .previous", () => {
168
+ const schemaV1 = defineSchema({
169
+ Node: defineRecord("Node", {
170
+ docs: "A node",
171
+ fields: {
172
+ label: P.String,
173
+ },
174
+ }),
175
+ });
176
+
177
+ const addWeight = defineSchemaUpdate(
178
+ "addWeight",
179
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
180
+ Node: schema.Node.addField("weight", P.Double).build(),
181
+ }),
182
+ );
183
+
184
+ const schemaV2 = nextSchema(schemaV1).addSchemaUpdate(addWeight).build();
185
+
186
+ // v2 points back to the v1 InitialSchema
187
+ expect(schemaV2.previous).toBe(schemaV1);
188
+ expect(schemaV2.previous.type).toBe("initial");
189
+ expect(schemaV2.version).toBe(2);
190
+ });
191
+
192
+ it("should build a multi-step version chain", () => {
193
+ const schemaV1 = defineSchema({
194
+ Item: defineRecord("Item", {
195
+ docs: "An item",
196
+ fields: { name: P.String },
197
+ }),
198
+ });
199
+
200
+ const schemaV2 = nextSchema(schemaV1)
201
+ .addSchemaUpdate(
202
+ defineSchemaUpdate(
203
+ "addPrice",
204
+ schema => ({
205
+ Item: schema.Item.addField("price", P.Double).build(),
206
+ }),
207
+ ),
208
+ )
209
+ .build();
210
+
211
+ const schemaV3 = nextSchema(schemaV2)
212
+ .addSchemaUpdate(
213
+ defineSchemaUpdate(
214
+ "addTags",
215
+ schema => ({
216
+ Item: schema.Item.addField("tags", P.Optional(P.String)).build(),
217
+ }),
218
+ ),
219
+ )
220
+ .build();
221
+
222
+ // Walk the chain: v3 (versioned) -> v2 (versioned) -> v1 (initial)
223
+ expect(schemaV3.version).toBe(3);
224
+ expect(schemaV3.previous).toBe(schemaV2);
225
+ expect(schemaV3.previous.type).toBe("versioned");
226
+ if (schemaV3.previous.type === "versioned") {
227
+ expect(schemaV3.previous.previous).toBe(schemaV1);
228
+ expect(schemaV3.previous.previous.type).toBe("initial");
229
+ }
230
+ });
231
+
232
+ it("should discriminate via type field on built schemas", () => {
233
+ const schemaV1 = defineSchema({
234
+ Item: defineRecord("Item", {
235
+ docs: "An item",
236
+ fields: { name: P.String },
237
+ }),
238
+ });
239
+
240
+ const schemaV2 = nextSchema(schemaV1)
241
+ .addSchemaUpdate(
242
+ defineSchemaUpdate(
243
+ "addPrice",
244
+ schema => ({
245
+ Item: schema.Item.addField("price", P.Double).build(),
246
+ }),
247
+ ),
248
+ )
249
+ .build();
250
+
251
+ expect(schemaV2.type).toBe("versioned");
252
+ expect(schemaV2.previous.type).toBe("initial");
253
+ });
254
+
255
+ it("should work with union schema updates", () => {
256
+ const CircleRecord = defineRecord("Circle", {
257
+ docs: "A circle",
258
+ fields: { radius: P.Double },
259
+ });
260
+
261
+ const RectangleRecord = defineRecord("Rectangle", {
262
+ docs: "A rectangle",
263
+ fields: { width: P.Double, height: P.Double },
264
+ });
265
+
266
+ const TriangleRecord = defineRecord("Triangle", {
267
+ docs: "A triangle",
268
+ fields: { base: P.Double, height: P.Double },
269
+ });
270
+
271
+ const schemaV1 = defineSchema({
272
+ Shape: defineUnion("Shape", {
273
+ docs: "A shape",
274
+ variants: {
275
+ circle: CircleRecord,
276
+ rectangle: RectangleRecord,
277
+ },
278
+ }),
279
+ });
280
+
281
+ const addTriangle = defineSchemaUpdate(
282
+ "addTriangle",
283
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
284
+ Shape: schema.Shape.addVariant("triangle", TriangleRecord).build(),
285
+ }),
286
+ );
287
+
288
+ const schemaV2 = nextSchema(schemaV1).addSchemaUpdate(addTriangle).build();
289
+
290
+ expect(schemaV2.models.Shape.variants.circle).toEqual({
291
+ type: "ref",
292
+ name: "Circle",
293
+ refType: "record",
294
+ });
295
+ expect(schemaV2.models.Shape.variants.triangle).toEqual({
296
+ type: "ref",
297
+ name: "Triangle",
298
+ refType: "record",
299
+ });
300
+ expect(schemaV2.version).toBe(2);
301
+
302
+ assertExactKeys<
303
+ typeof schemaV2.models.Shape.variants,
304
+ "circle" | "rectangle" | "triangle"
305
+ >();
306
+ });
307
+
308
+ it("should handle mixed record and union updates in one version", () => {
309
+ const PersonRecord = defineRecord("Person", {
310
+ docs: "A person",
311
+ fields: { name: P.String },
312
+ });
313
+
314
+ const OrganizationRecord = defineRecord("Organization", {
315
+ docs: "An organization",
316
+ fields: { orgName: P.String },
317
+ });
318
+
319
+ const schemaV1 = defineSchema({
320
+ Person: PersonRecord,
321
+ Entity: defineUnion("Entity", {
322
+ docs: "An entity",
323
+ variants: {
324
+ person: PersonRecord,
325
+ },
326
+ }),
327
+ });
328
+
329
+ const addAge = defineSchemaUpdate(
330
+ "addAge",
331
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
332
+ PersonV2: schema.Person.addField("age", P.Double).build(),
333
+ }),
334
+ );
335
+
336
+ const addOrgVariant = defineSchemaUpdate(
337
+ "addOrgVariant",
338
+ (schema: SchemaBuilder<typeof schemaV1.models>) => ({
339
+ Entity: schema.Entity.addVariant("organization", OrganizationRecord).build(),
340
+ }),
341
+ );
342
+
343
+ const schemaV2 = nextSchema(schemaV1)
344
+ .addSchemaUpdate(addAge)
345
+ .addSchemaUpdate(addOrgVariant)
346
+ .build();
347
+
348
+ expect(schemaV2.models.PersonV2.fields.name).toEqual({ type: "string" });
349
+ expect(schemaV2.models.PersonV2.fields.age).toEqual({ type: "double" });
350
+ expect(schemaV2.models.Entity.variants.person).toEqual({
351
+ type: "ref",
352
+ name: "Person",
353
+ refType: "record",
354
+ });
355
+ expect(schemaV2.models.Entity.variants.organization).toEqual({
356
+ type: "ref",
357
+ name: "Organization",
358
+ refType: "record",
359
+ });
360
+
361
+ assertHasKeys<typeof schemaV2.models, "Person" | "PersonV2" | "Entity">();
362
+ assertExactKeys<typeof schemaV2.models.PersonV2.fields, "name" | "age">();
363
+ assertExactKeys<typeof schemaV2.models.Entity.variants, "person" | "organization">();
364
+ });
365
+ });
@@ -65,15 +65,8 @@ describe("primitives", () => {
65
65
  });
66
66
  });
67
67
 
68
- it("should create nested arrays", () => {
69
- const nestedArray = P.Array(P.Array(P.String));
70
- expect(nestedArray).toEqual({
71
- type: "array",
72
- items: {
73
- type: "array",
74
- items: { type: "string" },
75
- },
76
- });
68
+ it("should reject nested arrays", () => {
69
+ expect(() => P.Array(P.Array(P.String))).toThrow(/Nested arrays/);
77
70
  });
78
71
 
79
72
  it("should create arrays of optional types", () => {
@@ -124,15 +117,8 @@ describe("primitives", () => {
124
117
  });
125
118
  });
126
119
 
127
- it("should handle nested optional types", () => {
128
- const doubleOptional = P.Optional(P.Optional(P.String));
129
- expect(doubleOptional).toEqual({
130
- type: "optional",
131
- item: {
132
- type: "optional",
133
- item: { type: "string" },
134
- },
135
- });
120
+ it("should reject nested optionals", () => {
121
+ expect(() => P.Optional(P.Optional(P.String))).toThrow(/Nested optionals/);
136
122
  });
137
123
 
138
124
  it("should create an optional boolean", () => {
@@ -167,23 +153,24 @@ describe("primitives", () => {
167
153
  });
168
154
 
169
155
  describe("Type combinations", () => {
170
- it("should create complex nested structures", () => {
171
- const complexType = P.Optional(P.Array(P.Optional(P.Array(P.String))));
156
+ it("should create optional array of optional strings", () => {
157
+ const complexType = P.Optional(P.Array(P.Optional(P.String)));
172
158
  expect(complexType).toEqual({
173
159
  type: "optional",
174
160
  item: {
175
161
  type: "array",
176
162
  items: {
177
163
  type: "optional",
178
- item: {
179
- type: "array",
180
- items: { type: "string" },
181
- },
164
+ item: { type: "string" },
182
165
  },
183
166
  },
184
167
  });
185
168
  });
186
169
 
170
+ it("should reject nested arrays even when inner array is wrapped in optional", () => {
171
+ expect(() => P.Array(P.Optional(P.Array(P.String)))).toThrow(/Nested arrays/);
172
+ });
173
+
187
174
  it("should maintain immutability of primitive types", () => {
188
175
  const string1 = P.String;
189
176
  const string2 = P.String;
@@ -16,16 +16,18 @@
16
16
 
17
17
  import type { UnionVariantArg } from "./defineUnion.js";
18
18
  import { resolveUnionVariantArg } from "./defineUnion.js";
19
- import type { RecordDef, RecordFields, UnionDef, UnionVariants } from "./defs.js";
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";
22
22
  import { isRecordDef, isUnionDef } from "./utils.js";
23
23
 
24
- export type ReturnedSchema = Record<string, RecordDef | UnionDef>;
24
+ /** Soon to be deprecated - Use `ModelDefs` instead. */
25
+ export type ReturnedSchema = ModelDefs;
25
26
 
26
- export type Schema<T extends ReturnedSchema> = T;
27
+ /** Soon to be deprecated - use `ModelDefs` instead. */
28
+ export type Schema<T extends ModelDefs> = T;
27
29
 
28
- export type SchemaBuilder<T extends ReturnedSchema> = {
30
+ export type SchemaBuilder<T extends ModelDefs> = {
29
31
  [k in keyof T]: T[k] extends UnionDef<infer R> ? UnionBuilder<R>
30
32
  : T[k] extends RecordDef<infer R> ? RecordBuilder<R>
31
33
  : never;
@@ -114,46 +116,28 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
114
116
  }
115
117
 
116
118
  export function defineMigration<
117
- const T extends ReturnedSchema,
118
- const S extends ReturnedSchema,
119
+ const T extends ModelDefs,
120
+ const S extends ModelDefs,
119
121
  >(
120
- previous: Schema<T>,
122
+ models: T,
121
123
  migration: (schema: SchemaBuilder<T>) => S,
122
- ): Schema<T & S> {
123
- /**
124
- * Create a map of builders for the schema.
125
- *
126
- * Note about type assertions:
127
- * We use a type assertion here (SchemaBuilder<T>) because TypeScript cannot
128
- * track the relationship between the keys in the original schema and the
129
- * corresponding builder types. This is a fundamental limitation of TypeScript's
130
- * type system when dealing with dynamically constructed objects.
131
- *
132
- * The assertion is safe because:
133
- * 1. We're only adding keys that exist in the original schema
134
- * 2. For each key, we're creating a builder of the correct type
135
- * 3. The resulting object exactly matches the SchemaBuilder<T> type
136
- */
124
+ ): T & S {
137
125
  const builders = {} as SchemaBuilder<T>;
138
126
 
139
- for (const key in previous) {
140
- const v = previous[key];
127
+ for (const key in models) {
128
+ const v = models[key];
141
129
 
142
- // Use our type guards for safer type narrowing
143
130
  if (isRecordDef(v)) {
144
- // This assertion is necessary because TypeScript can't track the field types
145
- // through the dynamic property access and object construction
146
131
  const recordBuilder = new RecordBuilderImpl(v);
147
132
  builders[key] = recordBuilder as unknown as SchemaBuilder<T>[typeof key];
148
133
  } else if (isUnionDef(v)) {
149
- // Similar assertion needed for union builders
150
134
  const unionBuilder = new UnionBuilderImpl(v);
151
135
  builders[key] = unionBuilder as unknown as SchemaBuilder<T>[typeof key];
152
136
  }
153
137
  }
154
138
 
155
139
  return {
156
- ...previous,
140
+ ...models,
157
141
  ...migration(builders),
158
142
  };
159
143
  }
@@ -0,0 +1,114 @@
1
+ /*
2
+ * Copyright 2026 Palantir Technologies, Inc. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { SchemaBuilder } from "./defineMigration.js";
18
+ import { defineMigration } from "./defineMigration.js";
19
+ import type { ModelDefs } from "./defs.js";
20
+
21
+ export interface InitialSchema<T extends ModelDefs = ModelDefs> {
22
+ readonly type: "initial";
23
+ readonly version: 1;
24
+ readonly models: T;
25
+ }
26
+
27
+ export interface FieldMigration {
28
+ readonly derivedFrom: readonly string[];
29
+ readonly forward: (oldFields: Record<string, unknown>) => unknown;
30
+ }
31
+
32
+ export type VersionMigrations = Record<string, Record<string, FieldMigration>>;
33
+
34
+ export interface VersionedSchema<T extends ModelDefs = ModelDefs> {
35
+ readonly type: "versioned";
36
+ readonly models: T;
37
+ readonly version: number;
38
+ readonly previous: SchemaDefinition;
39
+ readonly migrations?: VersionMigrations;
40
+ }
41
+
42
+ export type SchemaDefinition<T extends ModelDefs = ModelDefs> =
43
+ | InitialSchema<T>
44
+ | VersionedSchema<T>;
45
+
46
+ export function defineSchema<T extends ModelDefs>(models: T): InitialSchema<T> {
47
+ return { type: "initial", version: 1, models };
48
+ }
49
+
50
+ export interface SchemaUpdate<T extends ModelDefs, S extends ModelDefs> {
51
+ readonly name: string;
52
+ readonly migration: (schema: SchemaBuilder<T>) => S;
53
+ }
54
+
55
+ export function defineSchemaUpdate<T extends ModelDefs, S extends ModelDefs>(
56
+ name: string,
57
+ migration: (schema: SchemaBuilder<T>) => S,
58
+ ): SchemaUpdate<T, S> {
59
+ return { name, migration };
60
+ }
61
+
62
+ export interface SchemaVersionBuilder<T extends ModelDefs> {
63
+ addSchemaUpdate<S extends ModelDefs>(
64
+ update: SchemaUpdate<T, S>,
65
+ ): SchemaVersionBuilder<T & S>;
66
+ withMigrations(migrations: VersionMigrations): SchemaVersionBuilder<T>;
67
+ build(): VersionedSchema<T>;
68
+ }
69
+
70
+ class SchemaVersionBuilderImpl<T extends ModelDefs> implements SchemaVersionBuilder<T> {
71
+ private readonly models: T;
72
+ private readonly version: number;
73
+ private readonly previous: SchemaDefinition;
74
+ private readonly _migrations: VersionMigrations | undefined;
75
+
76
+ constructor(
77
+ models: T,
78
+ version: number,
79
+ previous: SchemaDefinition,
80
+ migrations?: VersionMigrations,
81
+ ) {
82
+ this.models = models;
83
+ this.version = version;
84
+ this.previous = previous;
85
+ this._migrations = migrations;
86
+ }
87
+
88
+ addSchemaUpdate<S extends ModelDefs>(
89
+ update: SchemaUpdate<T, S>,
90
+ ): SchemaVersionBuilder<T & S> {
91
+ const merged = defineMigration(this.models, update.migration);
92
+ return new SchemaVersionBuilderImpl(merged, this.version, this.previous, this._migrations);
93
+ }
94
+
95
+ withMigrations(migrations: VersionMigrations): SchemaVersionBuilder<T> {
96
+ return new SchemaVersionBuilderImpl(this.models, this.version, this.previous, migrations);
97
+ }
98
+
99
+ build(): VersionedSchema<T> {
100
+ return {
101
+ type: "versioned",
102
+ models: { ...this.models },
103
+ version: this.version,
104
+ previous: this.previous,
105
+ ...(this._migrations != null ? { migrations: this._migrations } : {}),
106
+ };
107
+ }
108
+ }
109
+
110
+ export function nextSchema<T extends ModelDefs>(
111
+ previous: SchemaDefinition<T>,
112
+ ): SchemaVersionBuilder<T> {
113
+ return new SchemaVersionBuilderImpl(previous.models, previous.version + 1, previous);
114
+ }
package/src/defs.ts CHANGED
@@ -44,3 +44,4 @@ export interface RecordDef<TFields extends RecordFields = RecordFields> extends
44
44
  }
45
45
 
46
46
  export type ModelDef = RecordDef | UnionDef;
47
+ export type ModelDefs = Record<string, ModelDef>;
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@
16
16
 
17
17
  export * from "./defineMigration.js";
18
18
  export * from "./defineRecord.js";
19
+ export * from "./defineSchemaUpdate.js";
19
20
  export * from "./defineUnion.js";
20
21
  export * from "./defs.js";
21
22
  export * from "./primitives.js";
package/src/primitives.ts CHANGED
@@ -101,11 +101,22 @@ export const String: String = { type: TypeKind.STRING };
101
101
  export const Double: Double = { type: TypeKind.DOUBLE };
102
102
  export const Boolean: Boolean = { type: TypeKind.BOOLEAN };
103
103
  export const Unknown: Unknown = { type: TypeKind.UNKNOWN };
104
- export const Array = <T extends TypeBase>(item: T): Array<T> => ({
105
- type: TypeKind.ARRAY,
106
- items: item,
107
- });
108
- export const Optional = <T extends TypeBase>(item: T): Optional<T> => ({
109
- type: TypeKind.OPTIONAL,
110
- item,
111
- });
104
+ function unwrapOneOptional(t: TypeBase): TypeBase {
105
+ return t.type === TypeKind.OPTIONAL && "item" in t ? (t as { item: TypeBase }).item : t;
106
+ }
107
+
108
+ export const Array = <T extends TypeBase>(item: T): Array<T> => {
109
+ if (unwrapOneOptional(item).type === TypeKind.ARRAY) {
110
+ throw new Error(
111
+ "Nested arrays (Array(Array(...)) or Array(Optional(Array(...)))) are not supported. "
112
+ + "Wrap the inner array in a record instead.",
113
+ );
114
+ }
115
+ return { type: TypeKind.ARRAY, items: item };
116
+ };
117
+ export const Optional = <T extends TypeBase>(item: T): Optional<T> => {
118
+ if (item.type === TypeKind.OPTIONAL) {
119
+ throw new Error("Nested optionals (Optional(Optional(...))) are not supported.");
120
+ }
121
+ return { type: TypeKind.OPTIONAL, item };
122
+ };