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.
- package/CHANGELOG.md +8 -0
- package/README.md +5 -2
- package/dist/cli/index.cjs +194 -54
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +2 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +194 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/migrate.cjs +194 -54
- package/dist/cli/migrate.cjs.map +1 -1
- package/dist/cli/migrate.js +194 -54
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/utils/index.d.cts +2 -2
- package/dist/cli/utils/index.d.ts +2 -2
- package/dist/{fields-B96iGprI.d.cts → fields-D2ZkO9Pe.d.cts} +33 -2
- package/dist/{fields-B96iGprI.d.ts → fields-D2ZkO9Pe.d.ts} +33 -2
- package/dist/index.cjs +28 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -6
- package/dist/index.js.map +1 -1
- package/dist/migration/analyzer.cjs +53 -9
- package/dist/migration/analyzer.cjs.map +1 -1
- package/dist/migration/analyzer.d.cts +2 -2
- package/dist/migration/analyzer.d.ts +2 -2
- package/dist/migration/analyzer.js +53 -9
- package/dist/migration/analyzer.js.map +1 -1
- package/dist/migration/diff.cjs +40 -24
- package/dist/migration/diff.cjs.map +1 -1
- package/dist/migration/diff.d.cts +2 -2
- package/dist/migration/diff.d.ts +2 -2
- package/dist/migration/diff.js +40 -24
- package/dist/migration/diff.js.map +1 -1
- package/dist/migration/engine.cjs +97 -20
- package/dist/migration/engine.cjs.map +1 -1
- package/dist/migration/engine.d.cts +5 -5
- package/dist/migration/engine.d.ts +5 -5
- package/dist/migration/engine.js +97 -20
- package/dist/migration/engine.js.map +1 -1
- package/dist/migration/generator.cjs +37 -1
- package/dist/migration/generator.cjs.map +1 -1
- package/dist/migration/generator.d.cts +2 -2
- package/dist/migration/generator.d.ts +2 -2
- package/dist/migration/generator.js +37 -1
- package/dist/migration/generator.js.map +1 -1
- package/dist/migration/index.cjs +195 -54
- package/dist/migration/index.cjs.map +1 -1
- package/dist/migration/index.d.cts +6 -6
- package/dist/migration/index.d.ts +6 -6
- package/dist/migration/index.js +195 -55
- package/dist/migration/index.js.map +1 -1
- package/dist/migration/snapshot.cjs +97 -20
- package/dist/migration/snapshot.cjs.map +1 -1
- package/dist/migration/snapshot.d.cts +3 -3
- package/dist/migration/snapshot.d.ts +3 -3
- package/dist/migration/snapshot.js +97 -20
- package/dist/migration/snapshot.js.map +1 -1
- package/dist/migration/utils/index.cjs +55 -0
- package/dist/migration/utils/index.cjs.map +1 -1
- package/dist/migration/utils/index.d.cts +3 -3
- package/dist/migration/utils/index.d.ts +3 -3
- package/dist/migration/utils/index.js +55 -1
- package/dist/migration/utils/index.js.map +1 -1
- package/dist/schema.cjs +28 -6
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +28 -6
- package/dist/schema.js.map +1 -1
- package/dist/server.cjs +223 -60
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/server.js +223 -61
- package/dist/server.js.map +1 -1
- package/dist/{type-mapper-D31WYDee.d.ts → type-mapper-DElLGPUA.d.ts} +21 -2
- package/dist/{type-mapper-BSPTj8t5.d.cts → type-mapper-DRxu96CN.d.cts} +21 -2
- package/dist/{types-CnzfX6JH.d.cts → types-Bs7N3Gwt.d.cts} +1 -1
- package/dist/{types-B_MLb1Ob.d.ts → types-BxJs7mmh.d.ts} +36 -7
- package/dist/{types-Do3jyFBm.d.ts → types-ByOyly4V.d.ts} +1 -1
- package/dist/{types-upZBVMfb.d.cts → types-CY5e-ORN.d.cts} +36 -7
- package/dist/{verify-DTzYpiXX.d.cts → verify-CkMwxocW.d.cts} +1 -1
- package/dist/{verify-BF72n2df.d.ts → verify-DqY4TpV6.d.ts} +1 -1
- 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()**
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
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, ...
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2083
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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] :
|
|
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
|
-
/**
|
|
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(
|
|
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 (
|
|
4573
|
+
if (value === null || value === void 0) {
|
|
4430
4574
|
return void 0;
|
|
4431
4575
|
}
|
|
4432
|
-
|
|
4576
|
+
const unsetValue = getFieldOptionUnsetValue(fieldType, key);
|
|
4577
|
+
if (unsetValue !== void 0 && areValuesEqual(value, unsetValue)) {
|
|
4433
4578
|
return void 0;
|
|
4434
4579
|
}
|
|
4435
|
-
if (key === "
|
|
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
|
-
|
|
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") {
|