@tailor-platform/sdk 1.49.0 → 1.50.1

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.
@@ -5796,6 +5796,20 @@ function formatBreakingChanges(breakingChanges) {
5796
5796
  }
5797
5797
  return lines.join("\n");
5798
5798
  }
5799
+ /**
5800
+ * Format warning changes for display
5801
+ * @param {WarningChangeInfo[]} warnings - Warning changes to format
5802
+ * @returns {string} Formatted warning changes string
5803
+ */
5804
+ function formatWarnings(warnings) {
5805
+ if (warnings.length === 0) return "";
5806
+ const lines = ["Warning: data loss possible:", ""];
5807
+ for (const w of warnings) {
5808
+ const location = w.fieldName ? `${w.typeName}.${w.fieldName}` : w.typeName;
5809
+ lines.push(` - ${location}: ${w.reason}`);
5810
+ }
5811
+ return lines.join("\n");
5812
+ }
5799
5813
  const DIFF_CHANGE_LABELS = {
5800
5814
  type_added: "type(s) added",
5801
5815
  type_removed: "type(s) removed",
@@ -5856,15 +5870,37 @@ const MIGRATION_NUMBER_PATTERN = /^\d{4}$/;
5856
5870
  */
5857
5871
  const DEFAULT_DECIMAL_SCALE = 6;
5858
5872
  /**
5859
- * Resolve the effective scale of a field for comparison purposes.
5860
- * Decimal fields without an explicit scale are stored on the platform with the
5861
- * default scale, so we normalize unset values to the default to avoid false drift.
5862
- * @param {SnapshotFieldConfig} field - Field configuration
5863
- * @returns {number | undefined} Effective scale, or undefined for non-decimal fields without scale
5873
+ * Normalize a snapshot field in place so the snapshot becomes the canonical
5874
+ * form for comparison. Currently fills in the platform default decimal scale
5875
+ * when omitted, which avoids false drift between local schemas (where scale
5876
+ * may be omitted) and the platform (which always materializes a scale).
5877
+ * @param {SnapshotFieldConfig} field - Field configuration to normalize
5878
+ */
5879
+ function normalizeSnapshotField(field) {
5880
+ if (field.type === "decimal" && field.scale === void 0) field.scale = 6;
5881
+ if (field.fields) for (const nested of Object.values(field.fields)) normalizeSnapshotField(nested);
5882
+ }
5883
+ /**
5884
+ * Normalize a snapshot type in place to the canonical comparison shape.
5885
+ * Currently fills:
5886
+ * - `pluralForm` via inflection when missing (legacy snapshots written
5887
+ * before `pluralForm` became required may omit it)
5888
+ * - per-field `scale` defaults via {@link normalizeSnapshotField}
5889
+ *
5890
+ * Idempotent — safe to call multiple times on the same input.
5891
+ * @param {TailorDBSnapshotType} type - Snapshot type to normalize
5892
+ */
5893
+ function normalizeSnapshotType(type) {
5894
+ if (!type.pluralForm) type.pluralForm = inflection.pluralize(type.name);
5895
+ for (const field of Object.values(type.fields)) normalizeSnapshotField(field);
5896
+ }
5897
+ /**
5898
+ * Type guard: is the operand a field-reference (object) operand?
5899
+ * @param {SnapshotPermissionOperand} operand - Operand to test
5900
+ * @returns {boolean} True if operand is a field-ref (not a value operand)
5864
5901
  */
5865
- function getEffectiveScale(field) {
5866
- if (field.scale !== void 0) return field.scale;
5867
- if (field.type === "decimal") return 6;
5902
+ function isSnapshotFieldRefOperand(operand) {
5903
+ return typeof operand === "object" && operand !== null && !Array.isArray(operand);
5868
5904
  }
5869
5905
  /**
5870
5906
  * Validate that a migration number follows the expected format (4-digit number)
@@ -5955,6 +5991,7 @@ function createSnapshotFieldConfig(field) {
5955
5991
  config.fields = {};
5956
5992
  for (const [nestedName, nestedConfig] of Object.entries(field.config.fields)) config.fields[nestedName] = createSnapshotFieldConfigFromOperatorConfig(nestedConfig);
5957
5993
  }
5994
+ normalizeSnapshotField(config);
5958
5995
  return config;
5959
5996
  }
5960
5997
  /**
@@ -6000,21 +6037,22 @@ function createSnapshotFieldConfigFromOperatorConfig(fieldConfig) {
6000
6037
  config.fields = {};
6001
6038
  for (const [nestedName, nestedConfig] of Object.entries(fieldConfig.fields)) config.fields[nestedName] = createSnapshotFieldConfigFromOperatorConfig(nestedConfig);
6002
6039
  }
6040
+ normalizeSnapshotField(config);
6003
6041
  return config;
6004
6042
  }
6005
6043
  /**
6006
6044
  * Create a snapshot type from a parsed type
6007
6045
  * @param {TailorDBType} type - Parsed TailorDB type definition
6008
- * @returns {SnapshotType} Snapshot type configuration
6046
+ * @returns {TailorDBSnapshotType} Snapshot type configuration
6009
6047
  */
6010
6048
  function createSnapshotType(type) {
6011
6049
  const fields = {};
6012
6050
  for (const [fieldName, field] of Object.entries(type.fields)) fields[fieldName] = createSnapshotFieldConfig(field);
6013
6051
  const snapshotType = {
6014
6052
  name: type.name,
6053
+ pluralForm: type.pluralForm || inflection.pluralize(type.name),
6015
6054
  fields
6016
6055
  };
6017
- if (type.pluralForm) snapshotType.pluralForm = type.pluralForm;
6018
6056
  if (type.description) snapshotType.description = type.description;
6019
6057
  if (type.settings) {
6020
6058
  snapshotType.settings = {};
@@ -6111,7 +6149,9 @@ function createSnapshotFromLocalTypes(types, namespace) {
6111
6149
  */
6112
6150
  function loadSnapshot(filePath) {
6113
6151
  const content = fs$1.readFileSync(filePath, "utf-8");
6114
- return JSON.parse(content);
6152
+ const snapshot = JSON.parse(content);
6153
+ for (const type of Object.values(snapshot.types)) normalizeSnapshotType(type);
6154
+ return snapshot;
6115
6155
  }
6116
6156
  /**
6117
6157
  * Load a migration diff from a file
@@ -6120,7 +6160,13 @@ function loadSnapshot(filePath) {
6120
6160
  */
6121
6161
  function loadDiff(filePath) {
6122
6162
  const content = fs$1.readFileSync(filePath, "utf-8");
6123
- return JSON.parse(content);
6163
+ const parsed = JSON.parse(content);
6164
+ const warnings = parsed.warnings ?? [];
6165
+ return {
6166
+ ...parsed,
6167
+ warnings,
6168
+ hasWarnings: warnings.length > 0
6169
+ };
6124
6170
  }
6125
6171
  /**
6126
6172
  * Get all migration directories and their files, sorted by number
@@ -6383,7 +6429,7 @@ function areFieldsDifferent(oldField, newField) {
6383
6429
  if (oldSerial.maxValue !== newSerial.maxValue) return true;
6384
6430
  if ((oldSerial.format ?? "") !== (newSerial.format ?? "")) return true;
6385
6431
  }
6386
- if (getEffectiveScale(oldField) !== getEffectiveScale(newField)) return true;
6432
+ if (oldField.scale !== newField.scale) return true;
6387
6433
  const oldFields = oldField.fields ?? {};
6388
6434
  const newFields = newField.fields ?? {};
6389
6435
  const oldFieldNames = Object.keys(oldFields);
@@ -6461,10 +6507,17 @@ function isBreakingFieldChange(typeName, fieldName, oldField, newField) {
6461
6507
  }
6462
6508
  function addChange(ctx, change, oldField, newField) {
6463
6509
  ctx.changes.push(change);
6464
- if (change.fieldName) {
6465
- const breaking = isBreakingFieldChange(change.typeName, change.fieldName, oldField, newField);
6466
- if (breaking) ctx.breakingChanges.push(breaking);
6510
+ if (!change.fieldName) return;
6511
+ const breaking = isBreakingFieldChange(change.typeName, change.fieldName, oldField, newField);
6512
+ if (breaking) {
6513
+ ctx.breakingChanges.push(breaking);
6514
+ return;
6467
6515
  }
6516
+ if (change.kind === "field_removed") ctx.warnings.push({
6517
+ typeName: change.typeName,
6518
+ fieldName: change.fieldName,
6519
+ reason: "Field removed (existing data will be dropped in the post-migration phase)"
6520
+ });
6468
6521
  }
6469
6522
  function compareTypeFields(ctx, typeName, prevType, currType) {
6470
6523
  const prevFieldNames = new Set(Object.keys(prevType.fields));
@@ -6655,9 +6708,12 @@ function comparePermissions(ctx, typeName, oldRecordPerm, newRecordPerm, oldGqlP
6655
6708
  * @returns {MigrationDiff} Migration diff between snapshots
6656
6709
  */
6657
6710
  function compareSnapshots(previous, current) {
6711
+ for (const type of Object.values(previous.types)) normalizeSnapshotType(type);
6712
+ for (const type of Object.values(current.types)) normalizeSnapshotType(type);
6658
6713
  const ctx = {
6659
6714
  changes: [],
6660
- breakingChanges: []
6715
+ breakingChanges: [],
6716
+ warnings: []
6661
6717
  };
6662
6718
  const previousTypeNames = new Set(Object.keys(previous.types));
6663
6719
  const currentTypeNames = new Set(Object.keys(current.types));
@@ -6666,11 +6722,17 @@ function compareSnapshots(previous, current) {
6666
6722
  typeName,
6667
6723
  after: current.types[typeName]
6668
6724
  });
6669
- for (const typeName of previousTypeNames) if (!currentTypeNames.has(typeName)) ctx.changes.push({
6670
- kind: "type_removed",
6671
- typeName,
6672
- before: previous.types[typeName]
6673
- });
6725
+ for (const typeName of previousTypeNames) if (!currentTypeNames.has(typeName)) {
6726
+ ctx.changes.push({
6727
+ kind: "type_removed",
6728
+ typeName,
6729
+ before: previous.types[typeName]
6730
+ });
6731
+ ctx.warnings.push({
6732
+ typeName,
6733
+ reason: "Type removed (all records of this type will be dropped in the post-migration phase)"
6734
+ });
6735
+ }
6674
6736
  for (const typeName of currentTypeNames) {
6675
6737
  if (!previousTypeNames.has(typeName)) continue;
6676
6738
  const prevType = previous.types[typeName];
@@ -6689,18 +6751,29 @@ function compareSnapshots(previous, current) {
6689
6751
  changes: ctx.changes,
6690
6752
  hasBreakingChanges: ctx.breakingChanges.length > 0,
6691
6753
  breakingChanges: ctx.breakingChanges,
6754
+ hasWarnings: ctx.warnings.length > 0,
6755
+ warnings: ctx.warnings,
6692
6756
  requiresMigrationScript: ctx.breakingChanges.length > 0
6693
6757
  };
6694
6758
  }
6695
6759
  /**
6696
- * Compare local types with a snapshot and generate a diff
6760
+ * Compare a snapshot against canonical TailorDBSnapshotType-shaped local types.
6761
+ * Callers are expected to pre-convert TailorDBService.types to TailorDBSnapshotType via
6762
+ * `createSnapshotType`. As a safety net, `compareSnapshots` re-runs idempotent
6763
+ * normalization on both sides, so a caller that forgets will still get correct
6764
+ * comparisons (no silent false drift).
6697
6765
  * @param {SchemaSnapshot} snapshot - Schema snapshot to compare against
6698
- * @param {Record<string, TailorDBType>} localTypes - Local type definitions
6766
+ * @param {Record<string, TailorDBSnapshotType>} localTypes - Local snapshot-shaped types
6699
6767
  * @param {string} namespace - Namespace for comparison
6700
6768
  * @returns {MigrationDiff} Migration diff
6701
6769
  */
6702
6770
  function compareLocalTypesWithSnapshot(snapshot, localTypes, namespace) {
6703
- return compareSnapshots(snapshot, createSnapshotFromLocalTypes(localTypes, namespace));
6771
+ return compareSnapshots(snapshot, {
6772
+ version: 1,
6773
+ namespace,
6774
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6775
+ types: localTypes
6776
+ });
6704
6777
  }
6705
6778
  /**
6706
6779
  * Validate migration files in a directory
@@ -6807,6 +6880,7 @@ function convertRemoteFieldsToSnapshot(remoteType) {
6807
6880
  ...remoteField.serial.format && { format: remoteField.serial.format }
6808
6881
  };
6809
6882
  if (remoteField.scale !== void 0) config.scale = remoteField.scale;
6883
+ normalizeSnapshotField(config);
6810
6884
  fields[fieldName] = config;
6811
6885
  }
6812
6886
  return fields;
@@ -6851,9 +6925,7 @@ function compareFields(typeName, fieldName, remoteField, snapshotField) {
6851
6925
  const remoteVector = remoteField.vector ?? false;
6852
6926
  const snapshotVector = snapshotField.vector ?? false;
6853
6927
  if (remoteVector !== snapshotVector) differences.push(`vector: remote=${remoteVector}, expected=${snapshotVector}`);
6854
- const remoteScale = getEffectiveScale(remoteField);
6855
- const snapshotScale = getEffectiveScale(snapshotField);
6856
- if (remoteScale !== snapshotScale) differences.push(`scale: remote=${remoteScale}, expected=${snapshotScale}`);
6928
+ if (remoteField.scale !== snapshotField.scale) differences.push(`scale: remote=${remoteField.scale}, expected=${snapshotField.scale}`);
6857
6929
  if (differences.length > 0) return {
6858
6930
  typeName,
6859
6931
  kind: "field_mismatch",
@@ -6873,6 +6945,7 @@ const SYSTEM_FIELDS = new Set(["id"]);
6873
6945
  * @returns {SchemaDrift[]} List of drifts detected
6874
6946
  */
6875
6947
  function compareRemoteWithSnapshot(remoteTypes, snapshot) {
6948
+ for (const type of Object.values(snapshot.types)) normalizeSnapshotType(type);
6876
6949
  const drifts = [];
6877
6950
  const remoteTypeMap = /* @__PURE__ */ new Map();
6878
6951
  for (const remoteType of remoteTypes) remoteTypeMap.set(remoteType.name, remoteType);
@@ -6938,6 +7011,189 @@ function formatSchemaDrifts(drifts) {
6938
7011
  return lines.join("\n");
6939
7012
  }
6940
7013
 
7014
+ //#endregion
7015
+ //#region src/cli/commands/tailordb/migrate/snapshot-manifest.ts
7016
+ /**
7017
+ * Convert a snapshot field config to proto format
7018
+ * @param {SnapshotFieldConfig} config - Snapshot field config
7019
+ * @returns {MessageInitShape<typeof TailorDBType_FieldConfigSchema>} Proto field config
7020
+ */
7021
+ function convertFieldConfigToProto(config) {
7022
+ const fieldEntry = {
7023
+ type: config.type,
7024
+ allowedValues: config.type === "enum" ? config.allowedValues?.map((v) => ({ ...v })) ?? [] : [],
7025
+ description: config.description || "",
7026
+ validate: toProtoSnapshotFieldValidate(config),
7027
+ array: config.array ?? false,
7028
+ index: config.index ?? false,
7029
+ unique: config.unique ?? false,
7030
+ foreignKey: config.foreignKey ?? false,
7031
+ foreignKeyType: config.foreignKeyType,
7032
+ foreignKeyField: config.foreignKeyField,
7033
+ required: config.required ?? true,
7034
+ vector: config.vector ?? false,
7035
+ ...toProtoSnapshotFieldHooks(config),
7036
+ ...config.serial && { serial: {
7037
+ start: BigInt(config.serial.start),
7038
+ ...config.serial.maxValue !== void 0 && { maxValue: BigInt(config.serial.maxValue) },
7039
+ ...config.serial.format && { format: config.serial.format }
7040
+ } },
7041
+ ...config.scale !== void 0 && { scale: config.scale }
7042
+ };
7043
+ if (config.type === "nested" && config.fields) fieldEntry.fields = processNestedFieldsFromSnapshot(config.fields);
7044
+ return fieldEntry;
7045
+ }
7046
+ function toProtoSnapshotFieldValidate(config) {
7047
+ return (config.validate ?? []).map((val) => ({
7048
+ action: TailorDBType_PermitAction.DENY,
7049
+ errorMessage: val.errorMessage || "",
7050
+ ...val.script && { script: { expr: val.script.expr ? `!${val.script.expr}` : "" } }
7051
+ }));
7052
+ }
7053
+ function toProtoSnapshotFieldHooks(config) {
7054
+ if (!config.hooks) return {};
7055
+ return { hooks: {
7056
+ create: config.hooks.create ? { expr: config.hooks.create.expr || "" } : void 0,
7057
+ update: config.hooks.update ? { expr: config.hooks.update.expr || "" } : void 0
7058
+ } };
7059
+ }
7060
+ /**
7061
+ * Process nested fields from snapshot format to proto format
7062
+ * @param {Record<string, SnapshotFieldConfig>} fields - Nested fields
7063
+ * @returns {Record<string, MessageInitShape<typeof TailorDBType_FieldConfigSchema>>} Proto nested fields
7064
+ */
7065
+ function processNestedFieldsFromSnapshot(fields) {
7066
+ const nestedFields = {};
7067
+ for (const [fieldName, fieldConfig] of Object.entries(fields)) if (fieldConfig.type === "nested" && fieldConfig.fields) {
7068
+ const deepNestedFields = processNestedFieldsFromSnapshot(fieldConfig.fields);
7069
+ nestedFields[fieldName] = {
7070
+ type: "nested",
7071
+ allowedValues: fieldConfig.allowedValues?.map((v) => ({ ...v })) ?? [],
7072
+ description: fieldConfig.description || "",
7073
+ validate: toProtoSnapshotFieldValidate(fieldConfig),
7074
+ required: fieldConfig.required ?? true,
7075
+ array: fieldConfig.array ?? false,
7076
+ index: false,
7077
+ unique: false,
7078
+ foreignKey: false,
7079
+ vector: false,
7080
+ ...toProtoSnapshotFieldHooks(fieldConfig),
7081
+ fields: deepNestedFields,
7082
+ ...fieldConfig.scale !== void 0 && { scale: fieldConfig.scale }
7083
+ };
7084
+ } else nestedFields[fieldName] = {
7085
+ type: fieldConfig.type,
7086
+ allowedValues: fieldConfig.type === "enum" ? fieldConfig.allowedValues?.map((v) => ({ ...v })) ?? [] : [],
7087
+ description: fieldConfig.description || "",
7088
+ validate: toProtoSnapshotFieldValidate(fieldConfig),
7089
+ required: fieldConfig.required ?? true,
7090
+ array: fieldConfig.array ?? false,
7091
+ index: false,
7092
+ unique: false,
7093
+ foreignKey: false,
7094
+ vector: false,
7095
+ ...toProtoSnapshotFieldHooks(fieldConfig),
7096
+ ...fieldConfig.serial && { serial: {
7097
+ start: BigInt(fieldConfig.serial.start),
7098
+ ...fieldConfig.serial.maxValue !== void 0 && { maxValue: BigInt(fieldConfig.serial.maxValue) },
7099
+ ...fieldConfig.serial.format && { format: fieldConfig.serial.format }
7100
+ } },
7101
+ ...fieldConfig.scale !== void 0 && { scale: fieldConfig.scale }
7102
+ };
7103
+ return nestedFields;
7104
+ }
7105
+
7106
+ //#endregion
7107
+ //#region src/cli/commands/tailordb/migrate/pre-migration-schema.ts
7108
+ /**
7109
+ * Pre-migration field config adjustments
7110
+ *
7111
+ * The Pre-phase sends a "relaxed" version of the target schema so that
7112
+ * `migrate.ts` scripts can still operate on the previous shape of the data.
7113
+ * This module handles the field-level adjustments:
7114
+ *
7115
+ * - `field_removed`: re-insert the removed field so migrate.ts can read it
7116
+ * (the physical drop happens in Post-phase).
7117
+ * - `field_added` with `required: true`: relax to `required: false`.
7118
+ * - `field_modified` optional→required, unique constraint added, enum
7119
+ * value removed: keep the looser side until Post-phase.
7120
+ *
7121
+ * Type-level deletions (`type_removed`) are handled by the deploy flow,
7122
+ * which retains the type until Post-phase rather than via this module.
7123
+ *
7124
+ * Post-phase then sends the final schema, after migrate.ts has had a chance
7125
+ * to fix up data.
7126
+ */
7127
+ /**
7128
+ * Diff change kinds that require pre-migration schema adjustments.
7129
+ */
7130
+ const PRE_MIGRATION_FIELD_KINDS = new Set([
7131
+ "field_added",
7132
+ "field_modified",
7133
+ "field_removed"
7134
+ ]);
7135
+ /**
7136
+ * Build a map of field changes that require pre-migration schema adjustment.
7137
+ * @param {PendingMigration[]} pendingMigrations - Pending migrations to scan
7138
+ * @returns {PreMigrationChangesMap} Map of changes keyed by typeName/fieldName
7139
+ */
7140
+ function buildPreMigrationChangesMap(pendingMigrations) {
7141
+ const map = /* @__PURE__ */ new Map();
7142
+ for (const migration of pendingMigrations) for (const change of migration.diff.changes) {
7143
+ if (!PRE_MIGRATION_FIELD_KINDS.has(change.kind)) continue;
7144
+ if (!change.fieldName) continue;
7145
+ const perType = map.get(change.typeName) ?? /* @__PURE__ */ new Map();
7146
+ perType.set(change.fieldName, change);
7147
+ map.set(change.typeName, perType);
7148
+ }
7149
+ return map;
7150
+ }
7151
+ /**
7152
+ * Apply pre-migration schema adjustments to a single field map in place.
7153
+ *
7154
+ * The fields map is the proto-shape `TailorDBType.schema.fields` that will
7155
+ * be sent in the Pre-phase. We mutate it so that:
7156
+ *
7157
+ * - Removed fields are re-inserted using their pre-migration config.
7158
+ * - Newly added required fields are relaxed to optional.
7159
+ * - Modified fields keep the looser side of unique/required/enum.
7160
+ *
7161
+ * @param {Record<string, MessageInitShape<typeof TailorDBType_FieldConfigSchema>>} fields - Field map to adjust (mutated in place)
7162
+ * @param {Map<string, DiffChange>} typeChanges - Changes for this type, keyed by fieldName
7163
+ */
7164
+ function applyPreMigrationFieldAdjustments(fields, typeChanges) {
7165
+ for (const [fieldName, change] of typeChanges) {
7166
+ if (change.kind === "field_removed") {
7167
+ const before = change.before;
7168
+ if (before) fields[fieldName] = convertFieldConfigToProto(before);
7169
+ continue;
7170
+ }
7171
+ const field = fields[fieldName];
7172
+ if (!field) continue;
7173
+ const before = change.before;
7174
+ const after = change.after;
7175
+ if (change.kind === "field_added" && after?.required) {
7176
+ field.required = false;
7177
+ continue;
7178
+ }
7179
+ if (change.kind !== "field_modified") continue;
7180
+ if (!before?.required && after?.required) field.required = false;
7181
+ if (!(before?.unique ?? false) && (after?.unique ?? false)) field.unique = false;
7182
+ if (before?.allowedValues && after?.allowedValues) {
7183
+ const afterValues = new Set(after.allowedValues.map((v) => v.value));
7184
+ if (before.allowedValues.filter((v) => !afterValues.has(v.value)).length > 0) {
7185
+ const valueMap = /* @__PURE__ */ new Map();
7186
+ for (const v of before.allowedValues) valueMap.set(v.value, v.description ?? "");
7187
+ for (const v of after.allowedValues) if (!valueMap.has(v.value)) valueMap.set(v.value, v.description ?? "");
7188
+ field.allowedValues = Array.from(valueMap.entries()).map(([value, description]) => ({
7189
+ value,
7190
+ description
7191
+ }));
7192
+ }
7193
+ }
7194
+ }
7195
+ }
7196
+
6941
7197
  //#endregion
6942
7198
  //#region src/cli/commands/tailordb/migrate/bundler.ts
6943
7199
  /**
@@ -7316,13 +7572,15 @@ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrat
7316
7572
  if (!fs$1.existsSync(diffPath)) continue;
7317
7573
  const diff = loadDiff(diffPath);
7318
7574
  const scriptPath = getMigrationFilePath(migrationsDir, file.number, "migrate");
7319
- if (diff.requiresMigrationScript && !fs$1.existsSync(scriptPath)) {
7575
+ const hasScript = fs$1.existsSync(scriptPath);
7576
+ if (diff.requiresMigrationScript && !hasScript) {
7320
7577
  logger.warn(`Migration ${namespace}/${file.number} requires a script but migrate.ts not found`);
7321
7578
  continue;
7322
7579
  }
7323
7580
  pendingMigrations.push({
7324
7581
  number: file.number,
7325
7582
  scriptPath,
7583
+ hasScript,
7326
7584
  diffPath,
7327
7585
  namespace,
7328
7586
  migrationsDir,
@@ -7387,7 +7645,7 @@ async function updateMigrationLabel(client, workspaceId, namespace, migrationNum
7387
7645
  * @returns {Promise<void>}
7388
7646
  */
7389
7647
  async function executeMigrations(context, migrations) {
7390
- const migrationsWithScripts = migrations.filter((m) => m.diff.requiresMigrationScript);
7648
+ const migrationsWithScripts = migrations.filter((m) => m.hasScript);
7391
7649
  if (migrationsWithScripts.length === 0) return;
7392
7650
  const migrationsByNamespace = groupMigrationsByNamespace(migrationsWithScripts);
7393
7651
  for (const [namespace, namespaceMigrations] of migrationsByNamespace) {
@@ -7553,7 +7811,7 @@ function formatRemoteVerificationResults(results) {
7553
7811
  * Validate migration files and detect pending migrations
7554
7812
  * @param {OperatorClient} client - Operator client instance
7555
7813
  * @param {string} workspaceId - Workspace ID
7556
- * @param {ReadonlyMap<string, Record<string, TailorDBType>>} typesByNamespace - Types by namespace
7814
+ * @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Types by namespace
7557
7815
  * @param {LoadedConfig} config - Loaded application config (includes path)
7558
7816
  * @param {boolean} noSchemaCheck - Whether to skip schema diff check
7559
7817
  * @returns {Promise<ValidateAndDetectResult>} Pending migrations and namespaces that have migration directories configured
@@ -7590,8 +7848,8 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
7590
7848
  pendingMigrations = await detectPendingMigrations(client, workspaceId, namespacesWithMigrations);
7591
7849
  if (pendingMigrations.length > 0) {
7592
7850
  logger.newline();
7593
- const withScripts = pendingMigrations.filter((m) => m.diff.requiresMigrationScript);
7594
- const withoutScripts = pendingMigrations.filter((m) => !m.diff.requiresMigrationScript);
7851
+ const withScripts = pendingMigrations.filter((m) => m.hasScript);
7852
+ const withoutScripts = pendingMigrations.filter((m) => !m.hasScript);
7595
7853
  logger.info(`Applying ${pendingMigrations.length} migration(s):`);
7596
7854
  if (withoutScripts.length > 0) logger.info(` • ${withoutScripts.length} schema change(s) (applied automatically with schema deployment)`, { mode: "plain" });
7597
7855
  if (withScripts.length > 0) logger.info(` • ${withScripts.length} data migration(s) (requires migration script execution)`, { mode: "plain" });
@@ -7660,25 +7918,23 @@ async function applyTailorDB(client, result, phase = "create-update") {
7660
7918
  const { changeSet, context: migrationContext } = result;
7661
7919
  if (phase === "create-update") {
7662
7920
  const typesByNamespace = /* @__PURE__ */ new Map();
7663
- for (const tailordb of migrationContext.application.tailorDBServices) {
7664
- const types = tailordb.types;
7665
- if (types) typesByNamespace.set(tailordb.namespace, types);
7666
- }
7921
+ for (const tailordb of migrationContext.tailorDBInputs) typesByNamespace.set(tailordb.namespace, tailordb.types);
7667
7922
  const { pendingMigrations, namespacesWithMigrations } = await validateAndDetectMigrations(client, migrationContext.workspaceId, typesByNamespace, migrationContext.config, migrationContext.noSchemaCheck);
7668
7923
  if (pendingMigrations.length > 0) {
7669
7924
  processedTypes.reset();
7670
7925
  deletedResources.reset();
7926
+ migrationSnapshotCache.reset();
7671
7927
  await executeServicesCreation(client, changeSet);
7672
- const migrationsRequiringScripts = pendingMigrations.filter((m) => m.diff.requiresMigrationScript);
7928
+ const migrationsRequiringScripts = pendingMigrations.filter((m) => m.hasScript);
7673
7929
  const migrationCtx = migrationsRequiringScripts.length > 0 ? buildMigrationContextForScripts(client, migrationContext, migrationsRequiringScripts) : void 0;
7674
7930
  if (migrationsRequiringScripts.length > 0) {
7675
7931
  logger.info(`Executing ${migrationsRequiringScripts.length} data migration(s)...`);
7676
7932
  logger.newline();
7677
7933
  }
7678
7934
  for (const migration of pendingMigrations) {
7679
- await executeSingleMigrationPrePhase(client, changeSet, migration);
7680
- if (migration.diff.requiresMigrationScript && migrationCtx) await executeMigrations(migrationCtx, [migration]);
7681
- await executeSingleMigrationPostPhase(client, changeSet, migration);
7935
+ await executeSingleMigrationPrePhase(client, changeSet, migration, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
7936
+ if (migration.hasScript && migrationCtx) await executeMigrations(migrationCtx, [migration]);
7937
+ await executeSingleMigrationPostPhase(client, changeSet, migration, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
7682
7938
  await updateMigrationLabel(client, migrationContext.workspaceId, migration.namespace, migration.number);
7683
7939
  }
7684
7940
  if (migrationsRequiringScripts.length > 0) {
@@ -7724,49 +7980,6 @@ function handleOptionalToRequiredError(error, messages) {
7724
7980
  throw error;
7725
7981
  }
7726
7982
  /**
7727
- * Build a map of breaking field changes from pending migrations
7728
- * @param {PendingMigration[]} pendingMigrations - Pending migrations
7729
- * @returns {BreakingChangesMap} Map of breaking changes
7730
- */
7731
- function buildBreakingChangesMap(pendingMigrations) {
7732
- const map = /* @__PURE__ */ new Map();
7733
- for (const migration of pendingMigrations) for (const change of migration.diff.changes) if (change.kind === "field_added" || change.kind === "field_modified" || change.kind === "field_removed") {
7734
- if (!change.fieldName) continue;
7735
- if (!map.has(change.typeName)) map.set(change.typeName, /* @__PURE__ */ new Map());
7736
- map.get(change.typeName).set(change.fieldName, change);
7737
- }
7738
- return map;
7739
- }
7740
- /**
7741
- * Apply pre-migration schema adjustments to avoid breaking changes before scripts run.
7742
- * @param fields - Field configs to adjust
7743
- * @param typeChanges - Breaking changes for a type
7744
- */
7745
- function applyPreMigrationFieldAdjustments(fields, typeChanges) {
7746
- for (const [fieldName, change] of typeChanges) {
7747
- const field = fields[fieldName];
7748
- if (!field) continue;
7749
- const before = change.before;
7750
- const after = change.after;
7751
- if (change.kind === "field_added" && after?.required) field.required = false;
7752
- if (change.kind !== "field_modified") continue;
7753
- if (!before?.required && after?.required) field.required = false;
7754
- if (!(before?.unique ?? false) && (after?.unique ?? false)) field.unique = false;
7755
- if (before?.allowedValues && after?.allowedValues) {
7756
- const afterValues = new Set(after.allowedValues.map((v) => v.value));
7757
- if (before.allowedValues.filter((v) => !afterValues.has(v.value)).length > 0) {
7758
- const valueMap = /* @__PURE__ */ new Map();
7759
- for (const v of before.allowedValues) valueMap.set(v.value, v.description ?? "");
7760
- for (const v of after.allowedValues) if (!valueMap.has(v.value)) valueMap.set(v.value, v.description ?? "");
7761
- field.allowedValues = Array.from(valueMap.entries()).map(([value, description]) => ({
7762
- value,
7763
- description
7764
- }));
7765
- }
7766
- }
7767
- }
7768
- }
7769
- /**
7770
7983
  * Get the set of type names affected by a migration
7771
7984
  * @param {PendingMigration} migration - Pending migration
7772
7985
  * @returns {Set<string>} Set of affected type names
@@ -7812,14 +8025,54 @@ const processedTypes = {
7812
8025
  }
7813
8026
  };
7814
8027
  /**
8028
+ * Snapshot cache for per-migration schema lookups during a single apply run.
8029
+ *
8030
+ * Only the initial baseline `0000/schema.json` is stored on disk; later migrations
8031
+ * ship `diff.json` only. To get the schema state AFTER migration N we replay the
8032
+ * initial snapshot through all diffs up to N via `reconstructSnapshotFromMigrations`.
8033
+ * Results are memoized per (namespace, migration number) for the apply run.
8034
+ */
8035
+ const migrationSnapshotCache = {
8036
+ cache: /* @__PURE__ */ new Map(),
8037
+ reset() {
8038
+ this.cache.clear();
8039
+ },
8040
+ load(migration) {
8041
+ const key = `${migration.namespace}/${migration.number}`;
8042
+ let snapshot = this.cache.get(key);
8043
+ if (!snapshot) {
8044
+ const reconstructed = reconstructSnapshotFromMigrations(migration.migrationsDir, migration.number);
8045
+ if (!reconstructed) throw new Error(`Cannot reconstruct snapshot for ${migration.namespace} migration ${migration.number}: no migrations found in ${migration.migrationsDir}`);
8046
+ snapshot = reconstructed;
8047
+ this.cache.set(key, snapshot);
8048
+ }
8049
+ return snapshot;
8050
+ }
8051
+ };
8052
+ /**
8053
+ * Build the TailorDBType manifest for `typeName` from migration N's snapshot.
8054
+ * @param migration - The pending migration whose snapshot to consult
8055
+ * @param typeName - The type name to look up in the snapshot
8056
+ * @param tailorDBInputs - Deploy inputs, used to resolve namespace gqlOperations
8057
+ * @param executorUsedTypes - Types used by executors (drives publishRecordEvents default)
8058
+ * @returns The manifest, or undefined if `typeName` is not in that snapshot.
8059
+ */
8060
+ function buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes) {
8061
+ const snapshotType = migrationSnapshotCache.load(migration).types[typeName];
8062
+ if (!snapshotType) return void 0;
8063
+ return generateTailorDBTypeManifest(snapshotType, executorUsedTypes, tailorDBInputs.find((i) => i.namespace === migration.namespace)?.config.gqlOperations);
8064
+ }
8065
+ /**
7815
8066
  * Execute pre-migration phase for a single migration
7816
8067
  * @param {OperatorClient} client - Operator client instance
7817
8068
  * @param {TailorDBChangeSet} changeSet - TailorDB change set
7818
8069
  * @param {PendingMigration} migration - Single pending migration
8070
+ * @param tailorDBInputs - Deploy inputs, used to resolve namespace gqlOperations for the snapshot
8071
+ * @param executorUsedTypes - Types used by executors (drives publishRecordEvents default)
7819
8072
  * @returns {Promise<void>} Promise that resolves when pre-migration phase completes
7820
8073
  */
7821
- async function executeSingleMigrationPrePhase(client, changeSet, migration) {
7822
- const breakingChanges = buildBreakingChangesMap([migration]);
8074
+ async function executeSingleMigrationPrePhase(client, changeSet, migration, tailorDBInputs, executorUsedTypes) {
8075
+ const preMigrationChanges = buildPreMigrationChangesMap([migration]);
7823
8076
  const affectedTypes = getAffectedTypeNames(migration);
7824
8077
  const createdBeforeMigration = new Set(processedTypes.created);
7825
8078
  await Promise.all([
@@ -7828,11 +8081,13 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration) {
7828
8081
  return typeName && affectedTypes.has(typeName) && !createdBeforeMigration.has(typeName);
7829
8082
  }).map((create) => {
7830
8083
  const typeName = create.request.tailordbType?.name;
8084
+ const snapshotType = typeName ? buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes) : void 0;
8085
+ if (!snapshotType) return void 0;
7831
8086
  if (typeName) processedTypes.created.add(typeName);
7832
- const typeChanges = typeName ? breakingChanges.get(typeName) : void 0;
7833
- if (!typeChanges || typeChanges.size === 0) return client.createTailorDBType(create.request);
7834
8087
  const clonedRequest = structuredClone(create.request);
7835
- if (clonedRequest.tailordbType?.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
8088
+ clonedRequest.tailordbType = snapshotType;
8089
+ const typeChanges = typeName ? preMigrationChanges.get(typeName) : void 0;
8090
+ if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType?.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
7836
8091
  return client.createTailorDBType(clonedRequest);
7837
8092
  }),
7838
8093
  ...changeSet.type.creates.filter((create) => {
@@ -7840,19 +8095,16 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration) {
7840
8095
  return typeName && affectedTypes.has(typeName) && createdBeforeMigration.has(typeName);
7841
8096
  }).map((create) => {
7842
8097
  const typeName = create.request.tailordbType?.name;
8098
+ const snapshotType = typeName ? buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes) : void 0;
8099
+ if (!snapshotType) return void 0;
7843
8100
  if (typeName) processedTypes.updated.add(typeName);
7844
- const typeChanges = typeName ? breakingChanges.get(typeName) : void 0;
7845
- if (!typeChanges || typeChanges.size === 0) return client.updateTailorDBType({
7846
- workspaceId: create.request.workspaceId,
7847
- namespaceName: create.request.namespaceName,
7848
- tailordbType: create.request.tailordbType
7849
- });
7850
- const clonedRequest = structuredClone(create.request);
7851
- if (clonedRequest.tailordbType?.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
8101
+ const clonedTypeRequest = structuredClone(snapshotType);
8102
+ const typeChanges = typeName ? preMigrationChanges.get(typeName) : void 0;
8103
+ if (typeChanges && typeChanges.size > 0 && clonedTypeRequest.schema?.fields) applyPreMigrationFieldAdjustments(clonedTypeRequest.schema.fields, typeChanges);
7852
8104
  return client.updateTailorDBType({
7853
8105
  workspaceId: create.request.workspaceId,
7854
8106
  namespaceName: create.request.namespaceName,
7855
- tailordbType: clonedRequest.tailordbType
8107
+ tailordbType: clonedTypeRequest
7856
8108
  });
7857
8109
  }),
7858
8110
  ...changeSet.type.updates.filter((update) => {
@@ -7860,11 +8112,13 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration) {
7860
8112
  return typeName && affectedTypes.has(typeName);
7861
8113
  }).map((update) => {
7862
8114
  const typeName = update.request.tailordbType?.name;
8115
+ const snapshotType = typeName ? buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes) : void 0;
8116
+ if (!snapshotType) return void 0;
7863
8117
  if (typeName) processedTypes.updated.add(typeName);
7864
- const typeChanges = typeName ? breakingChanges.get(typeName) : void 0;
7865
- if (!typeChanges || typeChanges.size === 0) return client.updateTailorDBType(update.request);
7866
8118
  const clonedRequest = structuredClone(update.request);
7867
- if (clonedRequest.tailordbType?.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
8119
+ clonedRequest.tailordbType = snapshotType;
8120
+ const typeChanges = typeName ? preMigrationChanges.get(typeName) : void 0;
8121
+ if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType?.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
7868
8122
  return client.updateTailorDBType(clonedRequest);
7869
8123
  })
7870
8124
  ]);
@@ -7901,24 +8155,40 @@ const deletedResources = {
7901
8155
  * @param {OperatorClient} client - Operator client instance
7902
8156
  * @param {TailorDBChangeSet} changeSet - TailorDB change set
7903
8157
  * @param {PendingMigration} migration - Single pending migration
8158
+ * @param tailorDBInputs - Deploy inputs, used to resolve namespace gqlOperations for the snapshot
8159
+ * @param executorUsedTypes - Types used by executors (drives publishRecordEvents default)
7904
8160
  * @returns {Promise<void>} Promise that resolves when post-migration phase completes
7905
8161
  */
7906
- async function executeSingleMigrationPostPhase(client, changeSet, migration) {
7907
- const breakingChanges = buildBreakingChangesMap([migration]);
8162
+ async function executeSingleMigrationPostPhase(client, changeSet, migration, tailorDBInputs, executorUsedTypes) {
8163
+ const preMigrationChanges = buildPreMigrationChangesMap([migration]);
7908
8164
  const affectedTypes = getAffectedTypeNames(migration);
7909
8165
  const deletedTypeNames = getDeletedTypeNames(migration);
7910
8166
  try {
7911
8167
  await Promise.all([...changeSet.type.creates.filter((create) => {
7912
8168
  const typeName = create.request.tailordbType?.name;
7913
- return typeName && affectedTypes.has(typeName) && breakingChanges.has(typeName);
7914
- }).map((create) => client.updateTailorDBType({
7915
- workspaceId: create.request.workspaceId,
7916
- namespaceName: create.request.namespaceName,
7917
- tailordbType: create.request.tailordbType
7918
- })), ...changeSet.type.updates.filter((update) => {
8169
+ return typeName && affectedTypes.has(typeName) && preMigrationChanges.has(typeName);
8170
+ }).map((create) => {
8171
+ const typeName = create.request.tailordbType?.name;
8172
+ const snapshotType = typeName ? buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes) : void 0;
8173
+ if (!snapshotType) return void 0;
8174
+ return client.updateTailorDBType({
8175
+ workspaceId: create.request.workspaceId,
8176
+ namespaceName: create.request.namespaceName,
8177
+ tailordbType: snapshotType
8178
+ });
8179
+ }), ...changeSet.type.updates.filter((update) => {
8180
+ const typeName = update.request.tailordbType?.name;
8181
+ return typeName && affectedTypes.has(typeName) && preMigrationChanges.has(typeName);
8182
+ }).map((update) => {
7919
8183
  const typeName = update.request.tailordbType?.name;
7920
- return typeName && affectedTypes.has(typeName) && breakingChanges.has(typeName);
7921
- }).map((update) => client.updateTailorDBType(update.request))]);
8184
+ const snapshotType = typeName ? buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes) : void 0;
8185
+ if (!snapshotType) return void 0;
8186
+ return client.updateTailorDBType({
8187
+ workspaceId: update.request.workspaceId,
8188
+ namespaceName: update.request.namespaceName,
8189
+ tailordbType: snapshotType
8190
+ });
8191
+ })]);
7922
8192
  } catch (error) {
7923
8193
  handleOptionalToRequiredError(error, ["This error occurred during post-migration phase. Please check your migration script.", "Ensure all existing records have values for fields being changed to required."]);
7924
8194
  }
@@ -7947,21 +8217,32 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration) {
7947
8217
  }
7948
8218
  }
7949
8219
  /**
7950
- * Plan TailorDB-related changes based on current and desired state.
7951
- * @param context - Planning context
7952
- * @returns Planned changes
8220
+ * Convert a runtime TailorDBService to the snapshot-shaped deploy input.
8221
+ * @param service - Loaded TailorDB service (after `loadTypes()`)
8222
+ * @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
7953
8223
  */
8224
+ function toTailorDBDeployInput(service) {
8225
+ const types = {};
8226
+ for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
8227
+ return {
8228
+ namespace: service.namespace,
8229
+ config: service.config,
8230
+ types
8231
+ };
8232
+ }
7954
8233
  async function planTailorDB(context) {
7955
8234
  const { client, workspaceId, application, forRemoval, config, noSchemaCheck, forceApplyAll = false } = context;
7956
8235
  const tailordbs = [];
7957
8236
  if (!forRemoval) for (const tailordb of application.tailorDBServices) {
7958
8237
  await tailordb.loadTypes();
7959
- tailordbs.push(tailordb);
8238
+ tailordbs.push(toTailorDBDeployInput(tailordb));
7960
8239
  }
7961
8240
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
8241
+ const executorUsedTypes = /* @__PURE__ */ new Set();
8242
+ for (const executor of executors) if (executor.trigger.kind === "tailordb") executorUsedTypes.add(executor.trigger.typeName);
7962
8243
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
7963
8244
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
7964
- const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executors, deletedServices, void 0, forceApplyAll), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
8245
+ const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, void 0, forceApplyAll), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
7965
8246
  return {
7966
8247
  changeSet: {
7967
8248
  service: serviceChangeSet,
@@ -7974,6 +8255,8 @@ async function planTailorDB(context) {
7974
8255
  context: {
7975
8256
  workspaceId,
7976
8257
  application,
8258
+ tailorDBInputs: tailordbs,
8259
+ executorUsedTypes,
7977
8260
  config,
7978
8261
  noSchemaCheck: noSchemaCheck ?? false
7979
8262
  }
@@ -8116,7 +8399,7 @@ async function planServices(client, workspaceId, appName, appId, tailordbs) {
8116
8399
  resourceOwners
8117
8400
  };
8118
8401
  }
8119
- async function planTypes(client, workspaceId, tailordbs, executors, deletedServices, filteredTypesByNamespace, forceApplyAll = false) {
8402
+ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, filteredTypesByNamespace, forceApplyAll = false) {
8120
8403
  const changeSet = createChangeSet("TailorDB types");
8121
8404
  const fetchTypes = (namespaceName) => {
8122
8405
  return fetchAll(async (pageToken, maxPageSize) => {
@@ -8134,8 +8417,6 @@ async function planTypes(client, workspaceId, tailordbs, executors, deletedServi
8134
8417
  }
8135
8418
  });
8136
8419
  };
8137
- const executorUsedTypes = /* @__PURE__ */ new Set();
8138
- for (const executor of executors) if (executor.trigger.kind === "tailordb") executorUsedTypes.add(executor.trigger.typeName);
8139
8420
  for (const tailordb of tailordbs) {
8140
8421
  const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
8141
8422
  for (const typeName of Object.keys(types)) {
@@ -8266,8 +8547,8 @@ function isNumericLikeValue(value) {
8266
8547
  return typeof value === "number" || typeof value === "bigint" || /^-?\d+$/.test(value);
8267
8548
  }
8268
8549
  /**
8269
- * Generate a TailorDB type manifest from parsed type
8270
- * @param {TailorDBType} type - Parsed TailorDB type
8550
+ * Generate a TailorDB type manifest from snapshot-shaped type
8551
+ * @param {TailorDBSnapshotType} type - Snapshot-shaped TailorDB type
8271
8552
  * @param {ReadonlySet<string>} executorUsedTypes - Set of types used by executors
8272
8553
  * @param {GqlOperations} [namespaceGqlOperations] - Default gqlOperations for the namespace (already normalized)
8273
8554
  * @returns {MessageInitShape<typeof TailorDBTypeSchema>} Type manifest
@@ -8294,7 +8575,7 @@ function generateTailorDBTypeManifest(type, executorUsedTypes, namespaceGqlOpera
8294
8575
  };
8295
8576
  const fields = {};
8296
8577
  Object.keys(type.fields).filter((fieldName) => fieldName !== "id").forEach((fieldName) => {
8297
- const fieldConfig = type.fields[fieldName].config;
8578
+ const fieldConfig = type.fields[fieldName];
8298
8579
  const fieldType = fieldConfig.type;
8299
8580
  const fieldEntry = {
8300
8581
  type: fieldType,
@@ -8307,7 +8588,7 @@ function generateTailorDBTypeManifest(type, executorUsedTypes, namespaceGqlOpera
8307
8588
  foreignKey: fieldConfig.foreignKey || false,
8308
8589
  foreignKeyType: fieldConfig.foreignKeyType,
8309
8590
  foreignKeyField: fieldConfig.foreignKeyField,
8310
- required: fieldConfig.required !== false,
8591
+ required: fieldConfig.required,
8311
8592
  vector: fieldConfig.vector || false,
8312
8593
  ...toProtoFieldHooks(fieldConfig),
8313
8594
  ...fieldConfig.serial && { serial: {
@@ -8321,14 +8602,14 @@ function generateTailorDBTypeManifest(type, executorUsedTypes, namespaceGqlOpera
8321
8602
  fields[fieldName] = fieldEntry;
8322
8603
  });
8323
8604
  const relationships = {};
8324
- for (const [relationName, rel] of Object.entries(type.forwardRelationships)) relationships[relationName] = {
8605
+ for (const [relationName, rel] of Object.entries(type.forwardRelationships ?? {})) relationships[relationName] = {
8325
8606
  refType: rel.targetType,
8326
8607
  refField: rel.sourceField,
8327
8608
  srcField: rel.targetField,
8328
8609
  array: rel.isArray,
8329
8610
  description: rel.description
8330
8611
  };
8331
- for (const [relationName, rel] of Object.entries(type.backwardRelationships)) relationships[relationName] = {
8612
+ for (const [relationName, rel] of Object.entries(type.backwardRelationships ?? {})) relationships[relationName] = {
8332
8613
  refType: rel.targetType,
8333
8614
  refField: rel.targetField,
8334
8615
  srcField: rel.sourceField,
@@ -8346,7 +8627,7 @@ function generateTailorDBTypeManifest(type, executorUsedTypes, namespaceGqlOpera
8346
8627
  if (type.files) Object.entries(type.files).forEach(([key, description]) => {
8347
8628
  files[key] = { description: description || "" };
8348
8629
  });
8349
- const permission = type.permissions.record ? protoPermission(type.permissions.record) : {
8630
+ const permission = type.permissions?.record ? protoPermission(type.permissions.record) : {
8350
8631
  create: [],
8351
8632
  read: [],
8352
8633
  update: [],
@@ -8392,7 +8673,7 @@ function processNestedFields(fields) {
8392
8673
  allowedValues: nestedFieldConfig.allowedValues || [],
8393
8674
  description: nestedFieldConfig.description || "",
8394
8675
  validate: toProtoFieldValidate(nestedFieldConfig),
8395
- required: nestedFieldConfig.required ?? true,
8676
+ required: nestedFieldConfig.required,
8396
8677
  array: nestedFieldConfig.array ?? false,
8397
8678
  index: false,
8398
8679
  unique: false,
@@ -8407,7 +8688,7 @@ function processNestedFields(fields) {
8407
8688
  allowedValues: nestedType === "enum" ? nestedFieldConfig.allowedValues || [] : [],
8408
8689
  description: nestedFieldConfig.description || "",
8409
8690
  validate: toProtoFieldValidate(nestedFieldConfig),
8410
- required: nestedFieldConfig.required ?? true,
8691
+ required: nestedFieldConfig.required,
8411
8692
  array: nestedFieldConfig.array ?? false,
8412
8693
  index: false,
8413
8694
  unique: false,
@@ -8425,9 +8706,12 @@ function processNestedFields(fields) {
8425
8706
  return nestedFields;
8426
8707
  }
8427
8708
  function protoPermission(permission) {
8428
- const ret = {};
8429
- for (const [key, policies] of Object.entries(permission)) ret[key] = policies.map((policy) => protoPolicy(policy));
8430
- return ret;
8709
+ return {
8710
+ create: permission.create.map((policy) => protoPolicy(policy)),
8711
+ read: permission.read.map((policy) => protoPolicy(policy)),
8712
+ update: permission.update.map((policy) => protoPolicy(policy)),
8713
+ delete: permission.delete.map((policy) => protoPolicy(policy))
8714
+ };
8431
8715
  }
8432
8716
  function protoPolicy(policy) {
8433
8717
  let permit;
@@ -8479,23 +8763,25 @@ function protoCondition(condition) {
8479
8763
  };
8480
8764
  }
8481
8765
  function protoOperand(operand) {
8482
- if (typeof operand === "object" && !Array.isArray(operand)) if ("user" in operand) return { kind: {
8483
- case: "userField",
8484
- value: operand.user
8485
- } };
8486
- else if ("record" in operand) return { kind: {
8487
- case: "recordField",
8488
- value: operand.record
8489
- } };
8490
- else if ("newRecord" in operand) return { kind: {
8491
- case: "newRecordField",
8492
- value: operand.newRecord
8493
- } };
8494
- else if ("oldRecord" in operand) return { kind: {
8495
- case: "oldRecordField",
8496
- value: operand.oldRecord
8497
- } };
8498
- else throw new Error(`Unknown operand: ${JSON.stringify(operand)}`);
8766
+ if (isSnapshotFieldRefOperand(operand)) {
8767
+ if ("user" in operand) return { kind: {
8768
+ case: "userField",
8769
+ value: operand.user
8770
+ } };
8771
+ if ("record" in operand) return { kind: {
8772
+ case: "recordField",
8773
+ value: operand.record
8774
+ } };
8775
+ if ("newRecord" in operand) return { kind: {
8776
+ case: "newRecordField",
8777
+ value: operand.newRecord
8778
+ } };
8779
+ if ("oldRecord" in operand) return { kind: {
8780
+ case: "oldRecordField",
8781
+ value: operand.oldRecord
8782
+ } };
8783
+ throw new Error(`Unknown field-ref operand shape: ${JSON.stringify(operand)}`);
8784
+ }
8499
8785
  return { kind: {
8500
8786
  case: "value",
8501
8787
  value: fromJson(ValueSchema, operand)
@@ -8527,7 +8813,7 @@ async function planGqlPermissions(client, workspaceId, tailordbs, deletedService
8527
8813
  });
8528
8814
  const types = tailordb.types;
8529
8815
  for (const typeName of Object.keys(types)) {
8530
- const gqlPermission = types[typeName].permissions.gql;
8816
+ const gqlPermission = types[typeName].permissions?.gql;
8531
8817
  if (!gqlPermission) continue;
8532
8818
  const desiredPermission = protoGqlPermission(gqlPermission);
8533
8819
  const existingPermission = existingGqlPermissions.find((entry) => entry.typeName === typeName);
@@ -8661,11 +8947,12 @@ function protoGqlCondition(condition) {
8661
8947
  };
8662
8948
  }
8663
8949
  function protoGqlOperand(operand) {
8664
- if (typeof operand === "object" && !Array.isArray(operand)) {
8950
+ if (isSnapshotFieldRefOperand(operand)) {
8665
8951
  if ("user" in operand) return { kind: {
8666
8952
  case: "userField",
8667
8953
  value: operand.user
8668
8954
  } };
8955
+ throw new Error(`Unsupported field-ref operand in GQL permission: ${JSON.stringify(operand)} — GQL permissions only support { user } field references`);
8669
8956
  }
8670
8957
  return { kind: {
8671
8958
  case: "value",
@@ -8674,7 +8961,7 @@ function protoGqlOperand(operand) {
8674
8961
  }
8675
8962
  /**
8676
8963
  * Check if there are schema differences between migration snapshots and local definitions
8677
- * @param {ReadonlyMap<string, Record<string, TailorDBType>>} typesByNamespace - Types by namespace
8964
+ * @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Snapshot-shaped local types by namespace
8678
8965
  * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
8679
8966
  * @returns {Promise<MigrationCheckResult[]>} Results for each namespace
8680
8967
  */
@@ -9130,6 +9417,20 @@ async function shouldForceApplyAll(client, workspaceId, application, functionEnt
9130
9417
  }
9131
9418
  return false;
9132
9419
  }
9420
+ /**
9421
+ * Decide which renamed-away applications should be deleted. Excludes the
9422
+ * target itself: id regeneration alone keeps the name unchanged, so deleting
9423
+ * by name would destroy the live app.
9424
+ * @param params - Inputs for the computation
9425
+ * @param params.conflicts - Detected owner conflicts across all services
9426
+ * @param params.resourceOwners - App names that still own resources we don't manage
9427
+ * @param params.targetAppName - The application currently being deployed
9428
+ * @returns Names of empty old applications that should be deleted
9429
+ */
9430
+ function computeRenamedAppDeletions(params) {
9431
+ const { conflicts, resourceOwners, targetAppName } = params;
9432
+ return [...new Set(conflicts.map((c) => c.currentOwner))].filter((owner) => !resourceOwners.has(owner) && owner !== targetAppName);
9433
+ }
9133
9434
  function printPlanResults(results) {
9134
9435
  const executorEntries = formatExecutorChangeEntries(results.executor.changeSet, buildPlannedExecutorsByName(results.executor.changeSet), results.functionRegistry.executorFunctionChanges);
9135
9436
  const resolverEntries = formatResolverChangeEntries(results.pipeline.changeSet.resolver, results.functionRegistry.resolverFunctionChanges);
@@ -9392,18 +9693,21 @@ async function deploy(options) {
9392
9693
  resourceName: del.name
9393
9694
  });
9394
9695
  await confirmImportantResourceDeletion(importantDeletions, yes);
9395
- const resourceOwners = new Set([
9396
- ...functionRegistry.resourceOwners,
9397
- ...tailorDB.resourceOwners,
9398
- ...staticWebsite.resourceOwners,
9399
- ...idp.resourceOwners,
9400
- ...auth.resourceOwners,
9401
- ...pipeline.resourceOwners,
9402
- ...executor.resourceOwners,
9403
- ...workflow.resourceOwners,
9404
- ...secretManager.resourceOwners
9405
- ]);
9406
- const emptyApps = [...new Set(allConflicts.map((c) => c.currentOwner))].filter((owner) => !resourceOwners.has(owner));
9696
+ const emptyApps = computeRenamedAppDeletions({
9697
+ conflicts: allConflicts,
9698
+ resourceOwners: new Set([
9699
+ ...functionRegistry.resourceOwners,
9700
+ ...tailorDB.resourceOwners,
9701
+ ...staticWebsite.resourceOwners,
9702
+ ...idp.resourceOwners,
9703
+ ...auth.resourceOwners,
9704
+ ...pipeline.resourceOwners,
9705
+ ...executor.resourceOwners,
9706
+ ...workflow.resourceOwners,
9707
+ ...secretManager.resourceOwners
9708
+ ]),
9709
+ targetAppName: application.name
9710
+ });
9407
9711
  for (const emptyApp of emptyApps) app.deletes.push({
9408
9712
  name: emptyApp,
9409
9713
  request: {
@@ -13324,7 +13628,7 @@ function generateEmptyDbTypes(namespace) {
13324
13628
  }
13325
13629
  /**
13326
13630
  * Generate table type definition from a snapshot type
13327
- * @param {SnapshotType} type - Snapshot type
13631
+ * @param {TailorDBSnapshotType} type - Snapshot type
13328
13632
  * @param {BreakingChangeFieldInfo} breakingChangeFields - Breaking change field info
13329
13633
  * @returns {{ typeDef: string; usedTimestamp: boolean; usedColumnType: boolean }} Generated type and utility type usage
13330
13634
  */
@@ -13552,8 +13856,11 @@ function generateMigrationScript(diff) {
13552
13856
  return `/**
13553
13857
  * Migration script for ${diff.namespace}
13554
13858
  *
13555
- * This script handles data migration for breaking schema changes.
13556
- * Edit this file to implement your data migration logic.
13859
+ * This script runs between the Pre-migration and Post-migration phases of
13860
+ * 'tailor-sdk deploy'. Use it to transform existing data so that the schema
13861
+ * change can complete safely (for breaking changes, this is hard-required;
13862
+ * for warning-tier changes it is optional). Edit this file to implement
13863
+ * your data migration logic.
13557
13864
  *
13558
13865
  * The transaction is managed by the deploy command.
13559
13866
  * If any operation fails, all changes will be rolled back.
@@ -13803,6 +14110,10 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
13803
14110
  logger.newline();
13804
14111
  }
13805
14112
  }
14113
+ if (diff.hasWarnings) {
14114
+ logger.newline();
14115
+ logger.warn(formatWarnings(diff.warnings));
14116
+ }
13806
14117
  const result = await generateDiffFiles(diff, migrationsDir, getNextMigrationNumber(migrationsDir), previousSnapshot, options.name);
13807
14118
  logger.success(`Generated migration ${styles.bold(result.migrationNumber.toString().padStart(4, "0"))}`);
13808
14119
  logger.info(` Diff file: ${result.diffFilePath}`);
@@ -13826,6 +14137,10 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
13826
14137
  } catch {
13827
14138
  return;
13828
14139
  }
14140
+ } else if (diff.hasWarnings) {
14141
+ logger.newline();
14142
+ logger.log(`Data loss is possible for this migration but no script was generated. To add a custom migrate.ts, run:`);
14143
+ logger.log(` ${styles.bold(`tailor-sdk tailordb migration script ${result.migrationNumber.toString().padStart(4, "0")} --namespace ${diff.namespace}`)}`);
13829
14144
  }
13830
14145
  }
13831
14146
  /**
@@ -15872,5 +16187,5 @@ function isDeno() {
15872
16187
  }
15873
16188
 
15874
16189
  //#endregion
15875
- export { deleteCommand$1 as $, getMigrationDirPath as $t, truncate as A, executionsCommand as At, updateOrganization as B, MIGRATION_LABEL_KEY as Bt, listCommand$2 as C, paginationArgs as Cn, jobsCommand as Ct, resumeWorkflow as D, startWorkflow as Dt, resumeCommand as E, startCommand as Et, showCommand as F, getCommand$6 as Ft, getCommand$1 as G, INITIAL_SCHEMA_NUMBER as Gt, treeCommand as H, bundleMigrationScript as Ht, logBetaWarning as I, getExecutor as It, updateFolder as J, compareLocalTypesWithSnapshot as Jt, getOrganization as K, MIGRATE_FILE_NAME as Kt, remove as L, deploy as Lt, generate as M, listWorkflowExecutions as Mt, generateCommand as N, functionExecutionStatusToString as Nt, listCommand$3 as O, getCommand$5 as Ot, show as P, formatKeyValueTable as Pt, getFolder as Q, getLatestMigrationNumber as Qt, removeCommand$1 as R, executeScript as Rt, listApps as S, pagedLogArgs as Sn, getExecutorJob as St, healthCommand as T, workspaceArgs as Tn, watchExecutorJob as Tt, listCommand$4 as U, DB_TYPES_FILE_NAME as Ut, organizationTree as V, parseMigrationLabelNumber as Vt, listOrganizations as W, DIFF_FILE_NAME as Wt, listFolders as X, createSnapshotFromLocalTypes as Xt, listCommand$5 as Y, compareSnapshots as Yt, getCommand$2 as Z, formatMigrationNumber as Zt, getWorkspace as _, defineAppCommand as _n, webhookCommand as _t, updateUser as a, reconstructSnapshotFromMigrations as an, getCommand$3 as at, createCommand as b, deploymentArgs as bn, listCommand$9 as bt, listCommand as c, hasChanges as cn, tokenCommand as ct, inviteUser as d, trnPrefix as dn, generate$1 as dt, getMigrationFilePath as en, deleteFolder as et, restoreCommand as f, generateUserTypes as fn, listCommand$8 as ft, getCommand as g, assertWritable as gn, listWebhookExecutors as gt, listWorkspaces as h, apiCall as hn, getFunctionRegistry as ht, updateCommand as i, loadDiff as in, listOAuth2Clients as it, truncateCommand as j, getWorkflowExecution as jt, listWorkflows as k, getWorkflow as kt, listUsers as l, getNamespacesWithMigrations as ln, listCommand$7 as lt, listCommand$1 as m, apiCommand as mn, getCommand$4 as mt, query as n, getNextMigrationNumber as nn, createFolder as nt, removeCommand as o, formatDiffSummary as on, getOAuth2Client as ot, restoreWorkspace as p, prompt as pn, listFunctionRegistries as pt, updateCommand$2 as q, SCHEMA_FILE_NAME as qt, queryCommand as r, isValidMigrationNumber as rn, listCommand$6 as rt, removeUser as s, formatMigrationDiff as sn, getMachineUserToken as st, isNativeTypeScriptRuntime as t, getMigrationFiles as tn, createCommand$1 as tt, inviteCommand as u, sdkNameLabelKey as un, listMachineUsers as ut, deleteCommand as v, commonArgs as vn, triggerCommand as vt, getAppHealth as w, toPageDirection as wn, listExecutorJobs as wt, createWorkspace as x, isVerbose as xn, listExecutors as xt, deleteWorkspace as y, confirmationArgs as yn, triggerExecutor as yt, updateCommand$1 as z, waitForExecution$1 as zt };
15876
- //# sourceMappingURL=runtime-oZgK353r.mjs.map
16190
+ export { listCommand$5 as $, compareSnapshots as $t, truncate as A, workspaceArgs as An, startCommand as At, logBetaWarning as B, getExecutor as Bt, listCommand$2 as C, configArg as Cn, triggerExecutor as Ct, resumeWorkflow as D, pagedLogArgs as Dn, jobsCommand as Dt, resumeCommand as E, isVerbose as En, getExecutorJob as Et, writeDbTypesFile as F, getWorkflowExecution as Ft, organizationTree as G, parseMigrationLabelNumber as Gt, removeCommand$1 as H, executeScript as Ht, getConfiguredEditorCommand as I, listWorkflowExecutions as It, listOrganizations as J, DIFF_FILE_NAME as Jt, treeCommand as K, bundleMigrationScript as Kt, openInConfiguredEditor as L, functionExecutionStatusToString as Lt, generate as M, getCommand$5 as Mt, generateCommand as N, getWorkflow as Nt, listCommand$3 as O, paginationArgs as On, listExecutorJobs as Ot, generateMigrationScript as P, executionsCommand as Pt, updateFolder as Q, compareLocalTypesWithSnapshot as Qt, show as R, formatKeyValueTable as Rt, listApps as S, commonArgs as Sn, triggerCommand as St, healthCommand as T, deploymentArgs as Tn, listExecutors as Tt, updateCommand$1 as U, waitForExecution$1 as Ut, remove as V, deploy as Vt, updateOrganization as W, MIGRATION_LABEL_KEY as Wt, getOrganization as X, MIGRATE_FILE_NAME as Xt, getCommand$1 as Y, INITIAL_SCHEMA_NUMBER as Yt, updateCommand$2 as Z, SCHEMA_FILE_NAME as Zt, getWorkspace as _, prompt as _n, listFunctionRegistries as _t, updateUser as a, getMigrationFiles as an, createCommand$1 as at, createCommand as b, assertWritable as bn, listWebhookExecutors as bt, listCommand as c, loadDiff as cn, listOAuth2Clients as ct, inviteUser as d, formatMigrationDiff as dn, getMachineUserToken as dt, createSnapshotFromLocalTypes as en, listFolders as et, restoreCommand as f, hasChanges as fn, tokenCommand as ft, getCommand as g, generateUserTypes as gn, listCommand$8 as gt, listWorkspaces as h, trnPrefix as hn, generate$1 as ht, updateCommand as i, getMigrationFilePath as in, deleteFolder as it, truncateCommand as j, startWorkflow as jt, listWorkflows as k, toPageDirection as kn, watchExecutorJob as kt, listUsers as l, reconstructSnapshotFromMigrations as ln, getCommand$3 as lt, listCommand$1 as m, sdkNameLabelKey as mn, listMachineUsers as mt, query as n, getLatestMigrationNumber as nn, getFolder as nt, removeCommand as o, getNextMigrationNumber as on, createFolder as ot, restoreWorkspace as p, getNamespacesWithMigrations as pn, listCommand$7 as pt, listCommand$4 as q, DB_TYPES_FILE_NAME as qt, queryCommand as r, getMigrationDirPath as rn, deleteCommand$1 as rt, removeUser as s, isValidMigrationNumber as sn, listCommand$6 as st, isNativeTypeScriptRuntime as t, formatMigrationNumber as tn, getCommand$2 as tt, inviteCommand as u, formatDiffSummary as un, getOAuth2Client as ut, deleteCommand as v, apiCommand as vn, getCommand$4 as vt, getAppHealth as w, confirmationArgs as wn, listCommand$9 as wt, createWorkspace as x, defineAppCommand as xn, webhookCommand as xt, deleteWorkspace as y, apiCall as yn, getFunctionRegistry as yt, showCommand as z, getCommand$6 as zt };
16191
+ //# sourceMappingURL=runtime-DgsMnMrO.mjs.map