@palantir/pack.schema 0.6.0 → 0.8.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.
@@ -117,9 +117,10 @@ export interface RecordBuilder<T extends Record<string, Type>> {
117
117
  type: V,
118
118
  options?: FieldOptions,
119
119
  ): RecordBuilder<T & { [k in K]: V }>;
120
- removeField<K extends keyof T & string>(
120
+ deprecateField<K extends keyof T & string>(
121
121
  name: K,
122
- ): RecordBuilder<Omit<T, K>>;
122
+ message?: string,
123
+ ): RecordBuilder<T>;
123
124
  build(): RecordDef<T>;
124
125
  }
125
126
 
@@ -174,14 +175,29 @@ class UnionBuilderImpl<S extends UnionVariants> implements UnionBuilder<S> {
174
175
  */
175
176
  const UPGRADE_OPTIONS_CARRIER = "__pack_upgrade_options__";
176
177
 
178
+ /**
179
+ * Non-enumerable carrier property attached to a `RecordDef` by
180
+ * `RecordBuilderImpl.build()` when the builder has collected one or more
181
+ * `deprecateField` calls. `applyMigration` reads this and then deletes the
182
+ * property, so the returned `RecordDef` is structurally clean. Maps field name
183
+ * → its deprecation info.
184
+ */
185
+ const DEPRECATED_FIELDS_CARRIER = "__pack_deprecated_fields__";
186
+
187
+ export interface FieldDeprecation {
188
+ readonly message?: string;
189
+ }
190
+
177
191
  class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
178
192
  private readonly name: string;
179
193
  private readonly fields: T;
180
194
  private readonly upgradeOptions: ReadonlyMap<string, FieldOptions>;
195
+ private readonly deprecatedFields: ReadonlyMap<string, FieldDeprecation>;
181
196
 
182
197
  constructor(
183
198
  initialRecordDef: RecordDef<T>,
184
199
  upgradeOptions?: ReadonlyMap<string, FieldOptions>,
200
+ deprecatedFields?: ReadonlyMap<string, FieldDeprecation>,
185
201
  ) {
186
202
  this.name = initialRecordDef.name;
187
203
  this.fields = { ...initialRecordDef.fields };
@@ -189,6 +205,7 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
189
205
  // they are NOT inherited from the prior version's RecordDef, because each
190
206
  // version's upgrades describe a specific version transition.
191
207
  this.upgradeOptions = new Map<string, FieldOptions>(upgradeOptions ?? []);
208
+ this.deprecatedFields = new Map<string, FieldDeprecation>(deprecatedFields ?? []);
192
209
  }
193
210
 
194
211
  // TODO: resolve field value from arg instead
@@ -212,24 +229,26 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
212
229
  docs: "",
213
230
  },
214
231
  next,
232
+ this.deprecatedFields,
215
233
  );
216
234
  }
217
235
 
218
- removeField<K extends keyof T & string>(
236
+ deprecateField<K extends keyof T & string>(
219
237
  name: K,
220
- ): RecordBuilder<Omit<T, K>> {
221
- const { [name]: _removed, ...rest } = this.fields;
222
- const next = new Map(this.upgradeOptions);
223
- next.delete(name);
238
+ message?: string,
239
+ ): RecordBuilder<T> {
240
+ const next = new Map(this.deprecatedFields);
241
+ next.set(name, { message });
224
242
  return new RecordBuilderImpl(
225
243
  {
226
244
  type: ModelDefType.RECORD,
227
245
  name: this.name,
228
- fields: rest as RecordFields,
246
+ fields: { ...this.fields },
229
247
  docs: "",
230
248
  },
249
+ this.upgradeOptions,
231
250
  next,
232
- ) as unknown as RecordBuilder<Omit<T, K>>;
251
+ );
233
252
  }
234
253
 
235
254
  build(): RecordDef<T> {
@@ -247,6 +266,14 @@ class RecordBuilderImpl<T extends RecordFields> implements RecordBuilder<T> {
247
266
  writable: false,
248
267
  });
249
268
  }
269
+ if (this.deprecatedFields.size > 0) {
270
+ Object.defineProperty(result, DEPRECATED_FIELDS_CARRIER, {
271
+ value: this.deprecatedFields,
272
+ enumerable: false,
273
+ configurable: true,
274
+ writable: false,
275
+ });
276
+ }
250
277
  return result;
251
278
  }
252
279
  }
@@ -270,22 +297,29 @@ export type FieldUpgrades = Record<
270
297
  Record<string, UpgradeFieldOptions<Record<string, unknown>>>
271
298
  >;
272
299
 
300
+ export type FieldDeprecations = Record<
301
+ string,
302
+ Record<string, FieldDeprecation>
303
+ >;
304
+
273
305
  export interface MigrationResult<M extends ModelDefs> {
274
306
  readonly models: M;
275
307
  readonly upgrades?: FieldUpgrades;
308
+ readonly deprecations?: FieldDeprecations;
276
309
  }
277
310
 
278
311
  /**
279
- * Lower-level entry point that returns both the merged models AND any
280
- * `UpgradeFieldOptions` collected via `addField(name, type, options)` during
281
- * the migration callback. Used by `addSchemaUpdate` to fold sugar-form
282
- * upgrades into a `VersionedSchema`'s migrations map.
312
+ * Lower-level entry point that returns the merged models plus the per-field
313
+ * metadata collected during the migration callback: `UpgradeFieldOptions` from
314
+ * `addField(name, type, options)` and deprecations from `deprecateField(name)`.
315
+ * Used by `addSchemaUpdate` to fold sugar-form upgrades into a
316
+ * `VersionedSchema`'s migrations map and deprecations into its deprecations map.
283
317
  *
284
- * Each `RecordBuilderImpl.build()` attaches its collected options to the
285
- * produced `RecordDef` via a non-enumerable carrier property; this function
286
- * walks the merged result, extracts the options keyed by the user's chosen
287
- * output key (so renames are handled), and strips the carrier so the returned
288
- * `RecordDef`s are structurally clean.
318
+ * Each `RecordBuilderImpl.build()` attaches its collected upgrade options and
319
+ * deprecations to the produced `RecordDef` via two non-enumerable carrier
320
+ * properties; this function walks the merged result, extracts both keyed by the
321
+ * user's chosen output key (so renames are handled), and strips the carriers so
322
+ * the returned `RecordDef`s are structurally clean.
289
323
  */
290
324
  export function applyMigration<
291
325
  const T extends ModelDefs,
@@ -298,35 +332,53 @@ export function applyMigration<
298
332
  const merged = { ...models, ...migration(builders) } as T & S;
299
333
 
300
334
  const upgrades: FieldUpgrades = {};
335
+ const deprecations: FieldDeprecations = {};
301
336
  for (const [modelKey, def] of Object.entries(merged)) {
302
337
  if (!isRecordDef(def)) continue;
303
338
  const carrier = def as unknown as Record<string, unknown>;
304
339
  const opts = carrier[UPGRADE_OPTIONS_CARRIER] as
305
340
  | ReadonlyMap<string, FieldOptions>
306
341
  | undefined;
307
- if (opts == null) continue;
308
- // Strip the carrier so the returned RecordDef is structurally clean.
309
- delete carrier[UPGRADE_OPTIONS_CARRIER];
310
- const fieldEntries: Record<
311
- string,
312
- UpgradeFieldOptions<Record<string, unknown>>
313
- > = {};
314
- for (const [fieldName, options] of opts) {
315
- // Only `UpgradeFieldOptions` (which declare `derivedFrom`) flow into the
316
- // upgrades map; `AdditiveFieldOptions` carry only a literal `default`
317
- // and have no read-time lens effect.
318
- if ("derivedFrom" in options) {
319
- fieldEntries[fieldName] = options;
342
+ if (opts != null) {
343
+ // Strip the carrier so the returned RecordDef is structurally clean.
344
+ delete carrier[UPGRADE_OPTIONS_CARRIER];
345
+ const fieldEntries: Record<
346
+ string,
347
+ UpgradeFieldOptions<Record<string, unknown>>
348
+ > = {};
349
+ for (const [fieldName, options] of opts) {
350
+ // Only `UpgradeFieldOptions` (which declare `derivedFrom`) flow into the
351
+ // upgrades map; `AdditiveFieldOptions` carry only a literal `default`
352
+ // and have no read-time lens effect.
353
+ if ("derivedFrom" in options) {
354
+ fieldEntries[fieldName] = options;
355
+ }
356
+ }
357
+ if (Object.keys(fieldEntries).length > 0) {
358
+ upgrades[modelKey] = fieldEntries;
320
359
  }
321
360
  }
322
- if (Object.keys(fieldEntries).length > 0) {
323
- upgrades[modelKey] = fieldEntries;
361
+
362
+ const deprecated = carrier[DEPRECATED_FIELDS_CARRIER] as
363
+ | ReadonlyMap<string, FieldDeprecation>
364
+ | undefined;
365
+ if (deprecated != null) {
366
+ // Strip the carrier so the returned RecordDef is structurally clean.
367
+ delete carrier[DEPRECATED_FIELDS_CARRIER];
368
+ const fieldEntries: Record<string, FieldDeprecation> = {};
369
+ for (const [fieldName, info] of deprecated) {
370
+ fieldEntries[fieldName] = info;
371
+ }
372
+ if (Object.keys(fieldEntries).length > 0) {
373
+ deprecations[modelKey] = fieldEntries;
374
+ }
324
375
  }
325
376
  }
326
377
 
327
378
  return {
328
379
  models: merged,
329
380
  upgrades: Object.keys(upgrades).length > 0 ? upgrades : undefined,
381
+ deprecations: Object.keys(deprecations).length > 0 ? deprecations : undefined,
330
382
  };
331
383
  }
332
384
 
@@ -14,9 +14,10 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import type { SchemaBuilder } from "./defineMigration.js";
17
+ import type { FieldDeprecation, SchemaBuilder } from "./defineMigration.js";
18
18
  import { applyMigration } from "./defineMigration.js";
19
19
  import type { ModelDefs } from "./defs.js";
20
+ import { isRecordDef } from "./utils.js";
20
21
 
21
22
  export interface InitialSchema<T extends ModelDefs = ModelDefs> {
22
23
  readonly type: "initial";
@@ -30,12 +31,19 @@ export interface FieldMigration {
30
31
 
31
32
  export type VersionMigrations = Record<string, Record<string, FieldMigration>>;
32
33
 
34
+ export interface FieldDeprecationInfo {
35
+ readonly fromVersion: number;
36
+ readonly message?: string;
37
+ }
38
+ export type VersionDeprecations = Record<string, Record<string, FieldDeprecationInfo>>;
39
+
33
40
  export interface VersionedSchema<T extends ModelDefs = ModelDefs> {
34
41
  readonly type: "versioned";
35
42
  readonly models: T;
36
43
  readonly version: number;
37
44
  readonly previous: SchemaDefinition;
38
45
  readonly migrations?: VersionMigrations;
46
+ readonly deprecations?: VersionDeprecations;
39
47
  }
40
48
 
41
49
  export type SchemaDefinition<T extends ModelDefs = ModelDefs> =
@@ -67,18 +75,18 @@ export interface SchemaVersionBuilder<T extends ModelDefs> {
67
75
  }
68
76
 
69
77
  /**
70
- * Merge two `VersionMigrations` records. Per-model field migrations from
71
- * `override` take precedence over `base` for the same field, but the maps are
72
- * combined at both the model and field level so explicit `withMigrations`
73
- * calls can supplement (not just replace) the sugar from `addField` options.
78
+ * Shallow-merge two `[modelKey][fieldName]` maps; `override` wins per field.
79
+ * Used for both migrations (so `withMigrations` supplements `addField` sugar)
80
+ * and deprecations (so each version's deprecations accumulate forward). The
81
+ * maps are combined at both the model and field level.
74
82
  */
75
- function mergeMigrations(
76
- base: VersionMigrations | undefined,
77
- override: VersionMigrations | undefined,
78
- ): VersionMigrations | undefined {
83
+ function mergeByModelField<V>(
84
+ base: Record<string, Record<string, V>> | undefined,
85
+ override: Record<string, Record<string, V>> | undefined,
86
+ ): Record<string, Record<string, V>> | undefined {
79
87
  if (base == null) return override;
80
88
  if (override == null) return base;
81
- const result: VersionMigrations = { ...base };
89
+ const result: Record<string, Record<string, V>> = { ...base };
82
90
  for (const [modelKey, fields] of Object.entries(override)) {
83
91
  result[modelKey] = { ...result[modelKey], ...fields };
84
92
  }
@@ -87,7 +95,7 @@ function mergeMigrations(
87
95
 
88
96
  /**
89
97
  * Drop migration entries that no longer correspond to a field on a record in
90
- * `models`. Required because `mergeMigrations` is monotonic — a later
98
+ * `models`. Required because `mergeByModelField` is monotonic — a later
91
99
  * `addSchemaUpdate` that removes or retypes a field would otherwise leave a
92
100
  * stale entry in the accumulator. Entries are dropped when:
93
101
  * - the model key is no longer present,
@@ -102,7 +110,7 @@ function pruneMigrations(
102
110
  const result: VersionMigrations = {};
103
111
  for (const [modelKey, fields] of Object.entries(migrations)) {
104
112
  const def = models[modelKey];
105
- if (def == null || def.type !== "record") continue;
113
+ if (!isRecordDef(def)) continue;
106
114
  const recordFields = def.fields;
107
115
  const kept: Record<string, FieldMigration> = {};
108
116
  for (const [fieldName, migration] of Object.entries(fields)) {
@@ -117,44 +125,82 @@ function pruneMigrations(
117
125
  return Object.keys(result).length > 0 ? result : undefined;
118
126
  }
119
127
 
128
+ /**
129
+ * Convert the version-agnostic `FieldDeprecations` collected by `applyMigration`
130
+ * (just `{ message? }`) into `VersionDeprecations` by stamping `fromVersion`.
131
+ */
132
+ function stampDeprecationVersions(
133
+ deprecations: Record<string, Record<string, FieldDeprecation>> | undefined,
134
+ version: number,
135
+ ): VersionDeprecations | undefined {
136
+ if (deprecations == null) return undefined;
137
+ const result: VersionDeprecations = {};
138
+ for (const [modelKey, fields] of Object.entries(deprecations)) {
139
+ const out: Record<string, FieldDeprecationInfo> = {};
140
+ for (const [fieldName, info] of Object.entries(fields)) {
141
+ out[fieldName] = { fromVersion: version, ...info };
142
+ }
143
+ result[modelKey] = out;
144
+ }
145
+ return result;
146
+ }
147
+
120
148
  class SchemaVersionBuilderImpl<T extends ModelDefs> implements SchemaVersionBuilder<T> {
121
149
  private readonly models: T;
122
150
  private readonly version: number;
123
151
  private readonly previous: SchemaDefinition;
124
152
  private readonly _migrations: VersionMigrations | undefined;
153
+ private readonly _deprecations: VersionDeprecations | undefined;
125
154
 
126
155
  constructor(
127
156
  models: T,
128
157
  version: number,
129
158
  previous: SchemaDefinition,
130
- migrations?: VersionMigrations,
159
+ migrations: VersionMigrations | undefined,
160
+ deprecations?: VersionDeprecations,
131
161
  ) {
132
162
  this.models = models;
133
163
  this.version = version;
134
164
  this.previous = previous;
135
165
  this._migrations = migrations;
166
+ this._deprecations = deprecations;
136
167
  }
137
168
 
138
169
  addSchemaUpdate<S extends ModelDefs>(
139
170
  update: SchemaUpdate<T, S>,
140
171
  ): SchemaVersionBuilder<T & S> {
141
- // applyMigration returns both the merged models and any sugar-form
142
- // upgrades collected via `addField(name, type, { derivedFrom, forward })`.
143
- const { models, upgrades } = applyMigration(this.models, update.migration);
144
- const merged = mergeMigrations(
172
+ // applyMigration returns the merged models, any sugar-form upgrades from
173
+ // `addField(name, type, { derivedFrom })`, and any `deprecateField` calls.
174
+ const { models, upgrades, deprecations } = applyMigration(this.models, update.migration);
175
+ const merged = mergeByModelField(
145
176
  this._migrations,
146
177
  upgrades as VersionMigrations | undefined,
147
178
  );
148
- // Reconcile against the new models: a later update that removes or
149
- // retypes a field must drop the prior step's stale migration entry.
179
+ // Reconcile against the new models: a later update that retypes a field
180
+ // must drop the prior step's stale migration entry.
150
181
  const pruned = pruneMigrations(merged, models);
151
- return new SchemaVersionBuilderImpl(models, this.version, this.previous, pruned);
182
+ // Attach this version to each freshly-deprecated field, then merge forward.
183
+ const thisVersionDeprecations = stampDeprecationVersions(deprecations, this.version);
184
+ const mergedDeprecations = mergeByModelField(this._deprecations, thisVersionDeprecations);
185
+ return new SchemaVersionBuilderImpl(
186
+ models,
187
+ this.version,
188
+ this.previous,
189
+ pruned,
190
+ mergedDeprecations,
191
+ );
152
192
  }
153
193
 
154
194
  withMigrations(migrations: VersionMigrations): SchemaVersionBuilder<T> {
155
- const merged = mergeMigrations(this._migrations, migrations);
195
+ const merged = mergeByModelField(this._migrations, migrations);
156
196
  const pruned = pruneMigrations(merged, this.models);
157
- return new SchemaVersionBuilderImpl(this.models, this.version, this.previous, pruned);
197
+ return new SchemaVersionBuilderImpl(
198
+ this.models,
199
+ this.version,
200
+ this.previous,
201
+ pruned,
202
+ this._deprecations,
203
+ );
158
204
  }
159
205
 
160
206
  build(): VersionedSchema<T> {
@@ -164,6 +210,7 @@ class SchemaVersionBuilderImpl<T extends ModelDefs> implements SchemaVersionBuil
164
210
  version: this.version,
165
211
  previous: this.previous,
166
212
  ...(this._migrations != null ? { migrations: this._migrations } : {}),
213
+ ...(this._deprecations != null ? { deprecations: this._deprecations } : {}),
167
214
  };
168
215
  }
169
216
  }
@@ -171,5 +218,13 @@ class SchemaVersionBuilderImpl<T extends ModelDefs> implements SchemaVersionBuil
171
218
  export function nextSchema<T extends ModelDefs>(
172
219
  previous: SchemaDefinition<T>,
173
220
  ): SchemaVersionBuilder<T> {
174
- return new SchemaVersionBuilderImpl(previous.models, previous.version + 1, previous);
221
+ const version = previous.version + 1;
222
+ const deprecations = "deprecations" in previous ? previous.deprecations : undefined;
223
+ return new SchemaVersionBuilderImpl(
224
+ previous.models,
225
+ version,
226
+ previous,
227
+ undefined,
228
+ deprecations,
229
+ );
175
230
  }