pocketbase-zod-schema 1.0.0 → 1.0.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -2
  3. package/dist/cli/index.cjs +194 -54
  4. package/dist/cli/index.cjs.map +1 -1
  5. package/dist/cli/index.d.cts +2 -2
  6. package/dist/cli/index.d.ts +2 -2
  7. package/dist/cli/index.js +194 -54
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/migrate.cjs +194 -54
  10. package/dist/cli/migrate.cjs.map +1 -1
  11. package/dist/cli/migrate.js +194 -54
  12. package/dist/cli/migrate.js.map +1 -1
  13. package/dist/cli/utils/index.d.cts +2 -2
  14. package/dist/cli/utils/index.d.ts +2 -2
  15. package/dist/{fields-B96iGprI.d.cts → fields-D2ZkO9Pe.d.cts} +33 -2
  16. package/dist/{fields-B96iGprI.d.ts → fields-D2ZkO9Pe.d.ts} +33 -2
  17. package/dist/index.cjs +28 -6
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +28 -6
  22. package/dist/index.js.map +1 -1
  23. package/dist/migration/analyzer.cjs +53 -9
  24. package/dist/migration/analyzer.cjs.map +1 -1
  25. package/dist/migration/analyzer.d.cts +2 -2
  26. package/dist/migration/analyzer.d.ts +2 -2
  27. package/dist/migration/analyzer.js +53 -9
  28. package/dist/migration/analyzer.js.map +1 -1
  29. package/dist/migration/diff.cjs +40 -24
  30. package/dist/migration/diff.cjs.map +1 -1
  31. package/dist/migration/diff.d.cts +2 -2
  32. package/dist/migration/diff.d.ts +2 -2
  33. package/dist/migration/diff.js +40 -24
  34. package/dist/migration/diff.js.map +1 -1
  35. package/dist/migration/engine.cjs +97 -20
  36. package/dist/migration/engine.cjs.map +1 -1
  37. package/dist/migration/engine.d.cts +5 -5
  38. package/dist/migration/engine.d.ts +5 -5
  39. package/dist/migration/engine.js +97 -20
  40. package/dist/migration/engine.js.map +1 -1
  41. package/dist/migration/generator.cjs +37 -1
  42. package/dist/migration/generator.cjs.map +1 -1
  43. package/dist/migration/generator.d.cts +2 -2
  44. package/dist/migration/generator.d.ts +2 -2
  45. package/dist/migration/generator.js +37 -1
  46. package/dist/migration/generator.js.map +1 -1
  47. package/dist/migration/index.cjs +195 -54
  48. package/dist/migration/index.cjs.map +1 -1
  49. package/dist/migration/index.d.cts +6 -6
  50. package/dist/migration/index.d.ts +6 -6
  51. package/dist/migration/index.js +195 -55
  52. package/dist/migration/index.js.map +1 -1
  53. package/dist/migration/snapshot.cjs +97 -20
  54. package/dist/migration/snapshot.cjs.map +1 -1
  55. package/dist/migration/snapshot.d.cts +3 -3
  56. package/dist/migration/snapshot.d.ts +3 -3
  57. package/dist/migration/snapshot.js +97 -20
  58. package/dist/migration/snapshot.js.map +1 -1
  59. package/dist/migration/utils/index.cjs +55 -0
  60. package/dist/migration/utils/index.cjs.map +1 -1
  61. package/dist/migration/utils/index.d.cts +3 -3
  62. package/dist/migration/utils/index.d.ts +3 -3
  63. package/dist/migration/utils/index.js +55 -1
  64. package/dist/migration/utils/index.js.map +1 -1
  65. package/dist/schema.cjs +28 -6
  66. package/dist/schema.cjs.map +1 -1
  67. package/dist/schema.d.cts +2 -2
  68. package/dist/schema.d.ts +2 -2
  69. package/dist/schema.js +28 -6
  70. package/dist/schema.js.map +1 -1
  71. package/dist/server.cjs +223 -60
  72. package/dist/server.cjs.map +1 -1
  73. package/dist/server.d.cts +5 -5
  74. package/dist/server.d.ts +5 -5
  75. package/dist/server.js +223 -61
  76. package/dist/server.js.map +1 -1
  77. package/dist/{type-mapper-D31WYDee.d.ts → type-mapper-DElLGPUA.d.ts} +21 -2
  78. package/dist/{type-mapper-BSPTj8t5.d.cts → type-mapper-DRxu96CN.d.cts} +21 -2
  79. package/dist/{types-CnzfX6JH.d.cts → types-Bs7N3Gwt.d.cts} +1 -1
  80. package/dist/{types-B_MLb1Ob.d.ts → types-BxJs7mmh.d.ts} +36 -7
  81. package/dist/{types-Do3jyFBm.d.ts → types-ByOyly4V.d.ts} +1 -1
  82. package/dist/{types-upZBVMfb.d.cts → types-CY5e-ORN.d.cts} +36 -7
  83. package/dist/{verify-DTzYpiXX.d.cts → verify-CkMwxocW.d.cts} +1 -1
  84. package/dist/{verify-BF72n2df.d.ts → verify-DqY4TpV6.d.ts} +1 -1
  85. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.1](https://github.com/dastron/pocketbase-zod-schema/compare/pocketbase-zod-schema-v1.0.0...pocketbase-zod-schema-v1.0.1) (2026-07-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * bug fixes for schema ([7007f4d](https://github.com/dastron/pocketbase-zod-schema/commit/7007f4dda37bef732cfcdb126e1a8dba79ece120))
9
+ * bug fixes for schema ([17bd233](https://github.com/dastron/pocketbase-zod-schema/commit/17bd23392a0455ff5b94d8cb54b30fbdcad7ffbc))
10
+
3
11
  ## [1.0.0](https://github.com/dastron/pocketbase-zod-schema/compare/pocketbase-zod-schema-v0.7.2...pocketbase-zod-schema-v1.0.0) (2026-07-28)
4
12
 
5
13
 
package/README.md CHANGED
@@ -212,7 +212,7 @@ The library provides explicit field helper functions for all PocketBase field ty
212
212
  | `SelectField(values, options?)` | select | Single or multiple select field | `status: SelectField(["draft", "published"])` |
213
213
  | `FileField(options?)` | file | Single file upload field | `avatar: FileField({ mimeTypes: ["image/*"] })` |
214
214
  | `FilesField(options?)` | file | Multiple file upload field | `images: FilesField({ maxSelect: 5 })` |
215
- | `JSONField(schema?)` | json | JSON field with optional schema | `metadata: JSONField()` |
215
+ | `JSONField(schema?, options?)` | json | JSON field with optional schema and size cap | `metadata: JSONField({ maxSize: "5M" })` |
216
216
  | `GeoPointField()` | geoPoint | Geographic coordinates field | `location: GeoPointField()` |
217
217
  | `RelationField(config)` | relation | Single relation field | `author: RelationField({ collection: "users" })` |
218
218
  | `RelationsField(config)` | relation | Multiple relation field | `tags: RelationsField({ collection: "tags" })` |
@@ -278,8 +278,11 @@ The library provides explicit field helper functions for all PocketBase field ty
278
278
  - `maxSelect?: number` - Maximum number of files allowed
279
279
  - Returns: `z.ZodType<string[], (File | string)[]>`
280
280
 
281
- **JSONField(schema?)**
281
+ **JSONField(schema?, options?)**
282
282
  - `schema?: z.ZodTypeAny` - Optional Zod schema for JSON structure validation
283
+ - `options?.maxSize?: number | "5M"` - Maximum serialized size, in bytes or with a `K`/`M`/`G`
284
+ suffix. PocketBase applies a 1MB default when unset, so larger payloads must set it. Either
285
+ argument may be passed alone: `JSONField({ maxSize: "5M" })`
283
286
  - Returns: `T | z.ZodRecord<z.ZodString, z.ZodAny>`
284
287
 
285
288
  **GeoPointField()**
@@ -496,6 +496,7 @@ var CollectionIdRegistry = class {
496
496
  };
497
497
 
498
498
  // src/migration/rule-validator.ts
499
+ var BACK_RELATION_SEGMENT = /^[a-zA-Z_][a-zA-Z0-9_]*_via_[a-zA-Z_][a-zA-Z0-9_]*$/;
499
500
  var RuleValidator = class {
500
501
  fields;
501
502
  collectionName;
@@ -571,7 +572,7 @@ var RuleValidator = class {
571
572
  /**
572
573
  * Extract field references from expression
573
574
  *
574
- * Matches field names that are not @request references.
575
+ * Matches field names that are not @request or @collection references.
575
576
  * Handles dot notation for relations: user.email, post.author.name
576
577
  *
577
578
  * @param expression - The rule expression
@@ -581,6 +582,7 @@ var RuleValidator = class {
581
582
  const refs = [];
582
583
  let cleaned = expression2.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''");
583
584
  cleaned = cleaned.replace(/@request\.[a-zA-Z_][a-zA-Z0-9_.]*/g, "");
585
+ cleaned = cleaned.replace(/@collection\.[a-zA-Z_][a-zA-Z0-9_.]*/g, "");
584
586
  const fieldPattern = /(?:^|[^@\w])([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)(?=[^a-zA-Z0-9_.]|$)/g;
585
587
  let match;
586
588
  while ((match = fieldPattern.exec(cleaned)) !== null) {
@@ -601,12 +603,26 @@ var RuleValidator = class {
601
603
  const keywords = ["true", "false", "null", "AND", "OR", "NOT", "LIKE", "IN"];
602
604
  return keywords.includes(word.toUpperCase());
603
605
  }
606
+ /**
607
+ * Check whether a path segment is a back-relation rather than a field
608
+ *
609
+ * A declared field wins: a collection may legitimately contain a field
610
+ * literally named `foo_via_bar`, and it should be validated as one.
611
+ *
612
+ * @param segment - A single dot-separated path segment
613
+ * @returns True if the segment should be treated as a back-relation
614
+ */
615
+ isBackRelationSegment(segment) {
616
+ return !this.fields.has(segment) && BACK_RELATION_SEGMENT.test(segment);
617
+ }
604
618
  /**
605
619
  * Validate a field reference exists in schema
606
620
  *
607
- * Checks if the root field exists and validates relation chains.
608
- * For nested references, warns about potential issues since we can't
609
- * validate across collections without loading related schemas.
621
+ * Only the root of the path is checked — the validator holds a single
622
+ * collection's fields, so anything past the first hop belongs to another
623
+ * collection and is accepted as-is. A root back-relation
624
+ * (`Members_via_WorkspaceRef.UserRef`) has no local field to check against
625
+ * either, so it is accepted too; PocketBase resolves it at runtime.
610
626
  *
611
627
  * @param fieldRef - The field reference to validate (e.g., "user" or "user.email")
612
628
  * @param result - The validation result to update
@@ -614,6 +630,9 @@ var RuleValidator = class {
614
630
  validateFieldReference(fieldRef, result) {
615
631
  const parts = fieldRef.split(".");
616
632
  const rootField = parts[0];
633
+ if (this.isBackRelationSegment(rootField)) {
634
+ return;
635
+ }
617
636
  if (!this.fields.has(rootField)) {
618
637
  result.errors.push(`Field '${rootField}' does not exist in collection '${this.collectionName}'`);
619
638
  result.valid = false;
@@ -624,10 +643,6 @@ var RuleValidator = class {
624
643
  if (field.type !== "relation") {
625
644
  result.errors.push(`Field '${rootField}' is not a relation field, cannot access nested property '${parts[1]}'`);
626
645
  result.valid = false;
627
- } else {
628
- result.warnings.push(
629
- `Nested field reference '${fieldRef}' - ensure target collection has field '${parts.slice(1).join(".")}'`
630
- );
631
646
  }
632
647
  }
633
648
  }
@@ -1016,14 +1031,35 @@ function isMultipleRelationField(fieldName, zodType) {
1016
1031
  const hasUppercase = /[A-Z]/.test(fieldName);
1017
1032
  return hasUppercase;
1018
1033
  }
1034
+ var REFERENCE_NAME_SUFFIXES = /* @__PURE__ */ new Set([
1035
+ "ref",
1036
+ "refs",
1037
+ "id",
1038
+ "ids",
1039
+ "uid",
1040
+ "uids",
1041
+ "uuid",
1042
+ "uuids",
1043
+ "fk",
1044
+ "fks",
1045
+ "pk",
1046
+ "pks"
1047
+ ]);
1019
1048
  function resolveTargetCollection(fieldName) {
1020
1049
  const matches = fieldName.match(/[A-Z][a-z]+/g);
1021
1050
  if (!matches || matches.length === 0) {
1022
1051
  return pluralize(fieldName);
1023
1052
  }
1024
1053
  const entityName = matches[matches.length - 1];
1054
+ if (REFERENCE_NAME_SUFFIXES.has(entityName.toLowerCase())) {
1055
+ throw new Error(buildUnresolvableTargetMessage(fieldName, entityName, matches.slice(0, -1)));
1056
+ }
1025
1057
  return pluralize(entityName);
1026
1058
  }
1059
+ function buildUnresolvableTargetMessage(fieldName, suffix, leadingSegments) {
1060
+ const likelyTarget = leadingSegments.length > 0 ? pluralize(leadingSegments.join("")) : "TargetCollection";
1061
+ return `Cannot infer the relation target for field "${fieldName}": the name ends in "${suffix}", which marks it as a reference without naming the collection it points at. Declare the target explicitly, e.g. ${fieldName}: RelationField({ collection: "${likelyTarget}" }).`;
1062
+ }
1027
1063
  function isRelationField(fieldName, zodType) {
1028
1064
  return isSingleRelationField(fieldName, zodType) || isMultipleRelationField(fieldName, zodType);
1029
1065
  }
@@ -1296,6 +1332,39 @@ function filterSupportedFieldOptions(fieldType, options) {
1296
1332
  }
1297
1333
  return Object.keys(filtered).length > 0 ? filtered : void 0;
1298
1334
  }
1335
+ var OPTION_UNSET_VALUES = {
1336
+ pattern: "",
1337
+ autogeneratePattern: "",
1338
+ cost: 0,
1339
+ maxSize: 0,
1340
+ maxSelect: 0,
1341
+ minSelect: 0,
1342
+ primaryKey: false,
1343
+ onlyInt: false,
1344
+ noDecimal: false,
1345
+ convertURLs: false,
1346
+ protected: false,
1347
+ hidden: false,
1348
+ presentable: false,
1349
+ system: false,
1350
+ values: [],
1351
+ mimeTypes: [],
1352
+ thumbs: [],
1353
+ exceptDomains: [],
1354
+ onlyDomains: []
1355
+ };
1356
+ function getFieldOptionUnsetValue(fieldType, key) {
1357
+ if (key === "min" || key === "max") {
1358
+ if (fieldType === "number") {
1359
+ return null;
1360
+ }
1361
+ if (fieldType === "date") {
1362
+ return "";
1363
+ }
1364
+ return 0;
1365
+ }
1366
+ return OPTION_UNSET_VALUES[key];
1367
+ }
1299
1368
  function dedentSql(value) {
1300
1369
  const lines = value.replace(/\r\n/g, "\n").split("\n");
1301
1370
  while (lines.length > 0 && lines[0].trim() === "") {
@@ -1661,7 +1730,8 @@ function buildFieldDefinition(fieldName, zodType) {
1661
1730
  } else {
1662
1731
  required2 = isFieldRequired(zodType);
1663
1732
  }
1664
- const { required: _required, ...options2 } = fieldMetadata.options || {};
1733
+ const { required: _required, ...metadataOptions } = fieldMetadata.options || {};
1734
+ const options2 = { ...extractChainedFieldOptions(fieldMetadata.type, unwrappedType), ...metadataOptions };
1665
1735
  const fieldDef2 = {
1666
1736
  name: fieldName,
1667
1737
  id: generateFieldId(fieldMetadata.type, fieldName),
@@ -1737,6 +1807,13 @@ function buildFieldDefinition(fieldName, zodType) {
1737
1807
  fieldDef.options = filterSupportedFieldOptions(fieldDef.type, fieldDef.options);
1738
1808
  return fieldDef;
1739
1809
  }
1810
+ var TYPES_READING_CHAINED_VALIDATORS = /* @__PURE__ */ new Set(["text", "password", "number", "select", "file"]);
1811
+ function extractChainedFieldOptions(fieldType, unwrappedType) {
1812
+ if (!TYPES_READING_CHAINED_VALIDATORS.has(fieldType)) {
1813
+ return {};
1814
+ }
1815
+ return extractFieldOptions(unwrappedType);
1816
+ }
1740
1817
  function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1741
1818
  const rawFields = extractFieldDefinitions(zodSchema);
1742
1819
  const explicitType = extractCollectionTypeFromSchema(zodSchema);
@@ -2057,12 +2134,26 @@ var FIELD_CONSTRUCTORS = {
2057
2134
  AutodateField,
2058
2135
  PasswordField
2059
2136
  };
2060
- function generateRuntimeFieldId(type) {
2061
- let digits = "";
2062
- for (let i = 0; i < 10; i++) {
2063
- digits += Math.floor(Math.random() * 10).toString();
2137
+ var crcTable = null;
2138
+ function crc32(input) {
2139
+ if (!crcTable) {
2140
+ crcTable = new Uint32Array(256);
2141
+ for (let i = 0; i < 256; i++) {
2142
+ let c = i;
2143
+ for (let bit = 0; bit < 8; bit++) {
2144
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2145
+ }
2146
+ crcTable[i] = c >>> 0;
2147
+ }
2148
+ }
2149
+ let crc = 4294967295;
2150
+ for (const byte of new TextEncoder().encode(input)) {
2151
+ crc = crcTable[(crc ^ byte) & 255] ^ crc >>> 8;
2064
2152
  }
2065
- return `${type || "field"}${digits}`;
2153
+ return (crc ^ 4294967295) >>> 0;
2154
+ }
2155
+ function generateRuntimeFieldId(type, name) {
2156
+ return `${type || "field"}${crc32(name)}`;
2066
2157
  }
2067
2158
 
2068
2159
  // src/migration/engine/fields-list.ts
@@ -2070,22 +2161,19 @@ var FieldsList = class {
2070
2161
  items = [];
2071
2162
  constructor(fields) {
2072
2163
  if (Array.isArray(fields)) {
2073
- for (const field of fields) {
2074
- this.items.push(field instanceof Field ? field : new Field(field));
2075
- }
2164
+ this.add(...fields);
2076
2165
  }
2077
2166
  }
2078
2167
  get length() {
2079
2168
  return this.items.length;
2080
2169
  }
2081
2170
  /**
2082
- * Appends fields; a field whose id matches an existing entry replaces it
2083
- * in place (position preserved), matching PocketBase upsert semantics.
2171
+ * Appends fields; a field that matches an existing entry replaces it in
2172
+ * place (position preserved), matching PocketBase upsert semantics.
2084
2173
  */
2085
2174
  add(...fields) {
2086
2175
  for (const raw of fields) {
2087
- const field = raw instanceof Field ? raw : new Field(raw);
2088
- const existing = this.indexOfId(field.id);
2176
+ const { field, existing } = this.resolveIncoming(raw);
2089
2177
  if (existing >= 0) {
2090
2178
  this.items[existing] = field;
2091
2179
  } else {
@@ -2094,15 +2182,14 @@ var FieldsList = class {
2094
2182
  }
2095
2183
  }
2096
2184
  /**
2097
- * Inserts fields at a position. A field with an already-present id is
2185
+ * Inserts fields at a position. A field matching an already-present entry is
2098
2186
  * moved: the old entry is removed first, then the field is inserted at
2099
2187
  * the requested position.
2100
2188
  */
2101
2189
  addAt(position, ...fields) {
2102
2190
  let insertAt = Math.max(0, Math.min(position, this.items.length));
2103
2191
  for (const raw of fields) {
2104
- const field = raw instanceof Field ? raw : new Field(raw);
2105
- const existing = this.indexOfId(field.id);
2192
+ const { field, existing } = this.resolveIncoming(raw);
2106
2193
  if (existing >= 0) {
2107
2194
  this.items.splice(existing, 1);
2108
2195
  if (existing < insertAt) {
@@ -2114,6 +2201,36 @@ var FieldsList = class {
2114
2201
  insertAt++;
2115
2202
  }
2116
2203
  }
2204
+ /**
2205
+ * Materializes an incoming field and locates the entry it replaces.
2206
+ *
2207
+ * PocketBase's rule (documented on `FieldsList.Add` in types.d.ts): match by
2208
+ * id, "or by their name if the new field doesn't have an explicit id", and
2209
+ * autogenerate a missing id from the name. Matching by id alone turns the
2210
+ * idiomatic `fields.add(new TextField({name: "title", max: 500}))` — no id,
2211
+ * meant to rewrite `title` — into a second field also called `title`, which
2212
+ * PocketBase would reject on save but the engine used to accept, corrupting
2213
+ * the reconstructed state.
2214
+ *
2215
+ * On a name match the *existing* id is kept rather than replaced with the
2216
+ * derived one. For a collection PocketBase authored the two are identical
2217
+ * (its auto-ids already are `<type><crc32(name)>`); they diverge only when
2218
+ * the stored field carries a hand-assigned id, and there preserving it keeps
2219
+ * a later `removeById`/`getById` in the same migration working.
2220
+ */
2221
+ resolveIncoming(raw) {
2222
+ const field = raw instanceof Field ? raw : new Field(raw);
2223
+ const hasExplicitId = typeof field.id === "string" && field.id !== "";
2224
+ if (hasExplicitId) {
2225
+ return { field, existing: this.indexOfId(field.id) };
2226
+ }
2227
+ const existing = this.items.findIndex((f) => f.name === field.name);
2228
+ field.id = existing >= 0 ? this.items[existing].id : generateRuntimeFieldId(
2229
+ typeof field.type === "string" ? field.type : "",
2230
+ typeof field.name === "string" ? field.name : ""
2231
+ );
2232
+ return { field, existing };
2233
+ }
2117
2234
  removeById(id) {
2118
2235
  const index = this.indexOfId(id);
2119
2236
  if (index >= 0) {
@@ -2126,12 +2243,18 @@ var FieldsList = class {
2126
2243
  this.items.splice(index, 1);
2127
2244
  }
2128
2245
  }
2246
+ /**
2247
+ * Null rather than undefined for a miss: these two return a Go `Field`
2248
+ * interface, and goja surfaces a nil interface to JavaScript as `null`. A
2249
+ * migration written as `if (collection.fields.getByName("x") === null)` has
2250
+ * to take the same branch here as it does in production.
2251
+ */
2129
2252
  getById(id) {
2130
2253
  const index = this.indexOfId(id);
2131
- return index >= 0 ? this.items[index] : void 0;
2254
+ return index >= 0 ? this.items[index] : null;
2132
2255
  }
2133
2256
  getByName(name) {
2134
- return this.items.find((f) => f.name === name);
2257
+ return this.items.find((f) => f.name === name) ?? null;
2135
2258
  }
2136
2259
  at(index) {
2137
2260
  return this.items.at(index);
@@ -2156,9 +2279,14 @@ var FieldsList = class {
2156
2279
  this.items = [];
2157
2280
  this.add(...fields);
2158
2281
  }
2159
- /** Plain objects, in order */
2282
+ /**
2283
+ * Plain objects, in order — copied deeply, so option arrays like a select
2284
+ * field's `values` are not shared with the live list. A shallow spread let a
2285
+ * snapshot handed to the diff engine alias engine state, where an in-place
2286
+ * sort or push downstream would silently rewrite the collection it came from.
2287
+ */
2160
2288
  serialize() {
2161
- return this.items.map((field) => ({ ...field }));
2289
+ return this.items.map((field) => copyPlainData({ ...field }));
2162
2290
  }
2163
2291
  indexOfId(id) {
2164
2292
  if (typeof id !== "string" || id === "") {
@@ -2167,6 +2295,22 @@ var FieldsList = class {
2167
2295
  return this.items.findIndex((f) => f.id === id);
2168
2296
  }
2169
2297
  };
2298
+ function copyPlainData(value) {
2299
+ if (Array.isArray(value)) {
2300
+ return value.map(copyPlainData);
2301
+ }
2302
+ if (value !== null && typeof value === "object" && !(value instanceof Date)) {
2303
+ const prototype = Object.getPrototypeOf(value);
2304
+ if (prototype === Object.prototype || prototype === null) {
2305
+ const copy = {};
2306
+ for (const [key, entry] of Object.entries(value)) {
2307
+ copy[key] = copyPlainData(entry);
2308
+ }
2309
+ return copy;
2310
+ }
2311
+ }
2312
+ return value;
2313
+ }
2170
2314
 
2171
2315
  // src/migration/engine/collection.ts
2172
2316
  var AUTH_SYSTEM_FIELDS = [
@@ -3585,6 +3729,13 @@ function createInertStub(name, options, warn) {
3585
3729
  if (prop === "then") {
3586
3730
  return void 0;
3587
3731
  }
3732
+ if (prop === "length") {
3733
+ return 0;
3734
+ }
3735
+ if (prop === Symbol.iterator) {
3736
+ return function* emptyIterator() {
3737
+ };
3738
+ }
3588
3739
  if (typeof prop === "symbol") {
3589
3740
  return void 0;
3590
3741
  }
@@ -3643,7 +3794,10 @@ var SimulatedAppImpl = class {
3643
3794
  ensureAuthSystemFields(collection);
3644
3795
  for (const field of collection.fields) {
3645
3796
  if (typeof field.id !== "string" || field.id === "") {
3646
- field.id = generateRuntimeFieldId(typeof field.type === "string" ? field.type : "");
3797
+ field.id = generateRuntimeFieldId(
3798
+ typeof field.type === "string" ? field.type : "",
3799
+ typeof field.name === "string" ? field.name : ""
3800
+ );
3647
3801
  }
3648
3802
  }
3649
3803
  this.store.upsert(collection);
@@ -4415,40 +4569,23 @@ function areValuesEqual(a, b) {
4415
4569
  }
4416
4570
  return a === b;
4417
4571
  }
4418
- var OPTION_ZERO_VALUES = {
4419
- autogeneratePattern: "",
4420
- pattern: "",
4421
- primaryKey: false,
4422
- convertURLs: false,
4423
- cost: 0,
4424
- hidden: false,
4425
- presentable: false,
4426
- system: false
4427
- };
4428
4572
  function normalizeOptionValue(key, value, fieldType) {
4429
- if (OPTION_ZERO_VALUES[key] !== void 0 && value === OPTION_ZERO_VALUES[key]) {
4573
+ if (value === null || value === void 0) {
4430
4574
  return void 0;
4431
4575
  }
4432
- if (key === "maxSelect" && value === 1 && (fieldType === "select" || fieldType === "file")) {
4576
+ const unsetValue = getFieldOptionUnsetValue(fieldType, key);
4577
+ if (unsetValue !== void 0 && areValuesEqual(value, unsetValue)) {
4433
4578
  return void 0;
4434
4579
  }
4435
- if (key === "maxSize" && value === 0 && fieldType === "file") {
4580
+ if (key === "values" && fieldType === "select" && Array.isArray(value)) {
4581
+ return [...value].sort();
4582
+ }
4583
+ if (key === "maxSelect" && value === 1 && (fieldType === "select" || fieldType === "file")) {
4436
4584
  return void 0;
4437
4585
  }
4438
4586
  if (key === "min" && value === 1 && fieldType === "number") {
4439
4587
  return void 0;
4440
4588
  }
4441
- if (fieldType === "file") {
4442
- if (key === "mimeTypes" && Array.isArray(value) && value.length === 0) {
4443
- return void 0;
4444
- }
4445
- if (key === "thumbs" && Array.isArray(value) && value.length === 0) {
4446
- return void 0;
4447
- }
4448
- if (key === "protected" && value === false) {
4449
- return void 0;
4450
- }
4451
- }
4452
4589
  if (fieldType === "autodate") {
4453
4590
  if (key === "onCreate" && value === true) {
4454
4591
  return void 0;
@@ -5268,13 +5405,16 @@ function generateFieldModification(collectionName, modification, varName, isLast
5268
5405
  const lines = [];
5269
5406
  const collectionVar = varName || `collection_${collectionName}_${modification.fieldName}`;
5270
5407
  const fieldVar = `${collectionVar}_field`;
5408
+ const fieldType = modification.newDefinition?.type ?? modification.currentDefinition?.type;
5271
5409
  lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
5272
5410
  lines.push(` const ${fieldVar} = ${collectionVar}.fields.getByName("${modification.fieldName}");`);
5273
5411
  lines.push(``);
5274
5412
  for (const change of modification.changes) {
5275
5413
  if (change.property.startsWith("options.")) {
5276
5414
  const optionKey = change.property.replace("options.", "");
5277
- lines.push(` ${fieldVar}.${optionKey} = ${formatValue(change.newValue)};`);
5415
+ const newValue = change.newValue === void 0 ? getFieldOptionUnsetValue(fieldType, optionKey) : change.newValue;
5416
+ const pbKey = fieldType === "number" && optionKey === "noDecimal" ? "onlyInt" : optionKey;
5417
+ lines.push(` ${fieldVar}.${pbKey} = ${formatValue(newValue)};`);
5278
5418
  } else if (change.property.startsWith("relation.")) {
5279
5419
  const relationKey = change.property.replace("relation.", "");
5280
5420
  if (relationKey === "collection") {