pocketbase-zod-schema 0.7.0 → 0.7.2

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 (70) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli/index.cjs +449 -50
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.d.cts +1 -1
  5. package/dist/cli/index.d.ts +1 -1
  6. package/dist/cli/index.js +449 -50
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/migrate.cjs +455 -53
  9. package/dist/cli/migrate.cjs.map +1 -1
  10. package/dist/cli/migrate.js +455 -53
  11. package/dist/cli/migrate.js.map +1 -1
  12. package/dist/cli/utils/index.cjs +8 -1
  13. package/dist/cli/utils/index.cjs.map +1 -1
  14. package/dist/cli/utils/index.d.cts +1 -1
  15. package/dist/cli/utils/index.d.ts +1 -1
  16. package/dist/cli/utils/index.js +8 -1
  17. package/dist/cli/utils/index.js.map +1 -1
  18. package/dist/index.cjs +90 -1
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +87 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/migration/analyzer.cjs +87 -14
  25. package/dist/migration/analyzer.cjs.map +1 -1
  26. package/dist/migration/analyzer.d.cts +12 -4
  27. package/dist/migration/analyzer.d.ts +12 -4
  28. package/dist/migration/analyzer.js +87 -15
  29. package/dist/migration/analyzer.js.map +1 -1
  30. package/dist/migration/diff.cjs +64 -9
  31. package/dist/migration/diff.cjs.map +1 -1
  32. package/dist/migration/diff.d.cts +12 -2
  33. package/dist/migration/diff.d.ts +12 -2
  34. package/dist/migration/diff.js +64 -10
  35. package/dist/migration/diff.js.map +1 -1
  36. package/dist/migration/generator.cjs +206 -39
  37. package/dist/migration/generator.cjs.map +1 -1
  38. package/dist/migration/generator.d.cts +54 -2
  39. package/dist/migration/generator.d.ts +54 -2
  40. package/dist/migration/generator.js +203 -40
  41. package/dist/migration/generator.js.map +1 -1
  42. package/dist/migration/index.cjs +501 -63
  43. package/dist/migration/index.cjs.map +1 -1
  44. package/dist/migration/index.d.cts +2 -2
  45. package/dist/migration/index.d.ts +2 -2
  46. package/dist/migration/index.js +501 -63
  47. package/dist/migration/index.js.map +1 -1
  48. package/dist/migration/snapshot.cjs +147 -1
  49. package/dist/migration/snapshot.cjs.map +1 -1
  50. package/dist/migration/snapshot.d.cts +3 -1
  51. package/dist/migration/snapshot.d.ts +3 -1
  52. package/dist/migration/snapshot.js +147 -1
  53. package/dist/migration/snapshot.js.map +1 -1
  54. package/dist/migration/utils/index.d.cts +1 -1
  55. package/dist/migration/utils/index.d.ts +1 -1
  56. package/dist/schema.cjs +90 -1
  57. package/dist/schema.cjs.map +1 -1
  58. package/dist/schema.d.cts +128 -2
  59. package/dist/schema.d.ts +128 -2
  60. package/dist/schema.js +87 -2
  61. package/dist/schema.js.map +1 -1
  62. package/dist/server.cjs +545 -65
  63. package/dist/server.cjs.map +1 -1
  64. package/dist/server.d.cts +2 -2
  65. package/dist/server.d.ts +2 -2
  66. package/dist/server.js +542 -66
  67. package/dist/server.js.map +1 -1
  68. package/dist/{types-CWHV6ATd.d.cts → types-CnzfX6JH.d.cts} +20 -2
  69. package/dist/{types-C2nGWHLV.d.ts → types-Do3jyFBm.d.ts} +20 -2
  70. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1193,6 +1193,60 @@ function unwrapZodType(zodType) {
1193
1193
  }
1194
1194
  return unwrapped;
1195
1195
  }
1196
+ function dedentSql(value) {
1197
+ const lines = value.replace(/\r\n/g, "\n").split("\n");
1198
+ while (lines.length > 0 && lines[0].trim() === "") {
1199
+ lines.shift();
1200
+ }
1201
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1202
+ lines.pop();
1203
+ }
1204
+ if (lines.length === 0) {
1205
+ return "";
1206
+ }
1207
+ let minIndent = Infinity;
1208
+ for (const line of lines) {
1209
+ if (line.trim() === "") continue;
1210
+ const indent = line.length - line.trimStart().length;
1211
+ if (indent < minIndent) {
1212
+ minIndent = indent;
1213
+ }
1214
+ }
1215
+ if (!Number.isFinite(minIndent) || minIndent === 0) {
1216
+ return lines.map((line) => line.trimEnd()).join("\n");
1217
+ }
1218
+ return lines.map((line) => line.slice(minIndent).trimEnd()).join("\n");
1219
+ }
1220
+ function stripLeadingComments(query) {
1221
+ let remaining = query.trim();
1222
+ while (remaining.length > 0) {
1223
+ if (remaining.startsWith("--")) {
1224
+ const newline = remaining.indexOf("\n");
1225
+ remaining = newline === -1 ? "" : remaining.slice(newline + 1).trim();
1226
+ continue;
1227
+ }
1228
+ if (remaining.startsWith("/*")) {
1229
+ const end = remaining.indexOf("*/");
1230
+ remaining = end === -1 ? "" : remaining.slice(end + 2).trim();
1231
+ continue;
1232
+ }
1233
+ break;
1234
+ }
1235
+ return remaining;
1236
+ }
1237
+ function validateViewQuery(collectionName, viewQuery) {
1238
+ if (typeof viewQuery !== "string" || viewQuery.trim() === "") {
1239
+ throw new Error(
1240
+ `View collection "${collectionName}" requires a non-empty viewQuery. Provide the SQL SELECT statement backing the view.`
1241
+ );
1242
+ }
1243
+ const body = stripLeadingComments(viewQuery);
1244
+ if (!/^(select|with)\b/i.test(body)) {
1245
+ throw new Error(
1246
+ `View collection "${collectionName}" has an invalid viewQuery: it must start with SELECT or WITH. Received: ${body.slice(0, 40)}${body.length > 40 ? "..." : ""}`
1247
+ );
1248
+ }
1249
+ }
1196
1250
  function getCollectionNameFromFile(filePath) {
1197
1251
  const filename = path8.basename(filePath).replace(/\.(ts|js)$/, "");
1198
1252
  return toCollectionName(filename);
@@ -1216,13 +1270,26 @@ function extractCollectionTypeFromSchema(zodSchema) {
1216
1270
  }
1217
1271
  try {
1218
1272
  const metadata = JSON.parse(zodSchema.description);
1219
- if (metadata.type === "base" || metadata.type === "auth") {
1273
+ if (metadata.type === "base" || metadata.type === "auth" || metadata.type === "view") {
1220
1274
  return metadata.type;
1221
1275
  }
1222
1276
  } catch {
1223
1277
  }
1224
1278
  return null;
1225
1279
  }
1280
+ function extractViewQueryFromSchema(zodSchema) {
1281
+ if (!zodSchema.description) {
1282
+ return null;
1283
+ }
1284
+ try {
1285
+ const metadata = JSON.parse(zodSchema.description);
1286
+ if (typeof metadata.viewQuery === "string") {
1287
+ return metadata.viewQuery;
1288
+ }
1289
+ } catch {
1290
+ }
1291
+ return null;
1292
+ }
1226
1293
  function extractSchemaDefinitions(module, patterns = ["Schema", "InputSchema", "Collection"]) {
1227
1294
  const result = {};
1228
1295
  if (module.default instanceof z.ZodObject) {
@@ -1381,6 +1448,15 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1381
1448
  const rawFields = extractFieldDefinitions(zodSchema);
1382
1449
  const explicitType = extractCollectionTypeFromSchema(zodSchema);
1383
1450
  const collectionType = explicitType ?? (isAuthCollection(rawFields) ? "auth" : "base");
1451
+ const isView = collectionType === "view";
1452
+ const viewQuery = extractViewQueryFromSchema(zodSchema);
1453
+ if (isView) {
1454
+ validateViewQuery(collectionName, viewQuery);
1455
+ } else if (viewQuery !== null) {
1456
+ console.warn(
1457
+ `[${collectionName}] viewQuery is only used by view collections and will be ignored. Use defineView() or set type: "view" to define a view collection.`
1458
+ );
1459
+ }
1384
1460
  const fields = rawFields.filter((f) => !["created", "updated"].includes(f.name)).map(({ name, zodType }) => buildFieldDefinition(name, zodType));
1385
1461
  if (collectionType === "auth") {
1386
1462
  const authSystemFields = [
@@ -1440,26 +1516,43 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1440
1516
  }
1441
1517
  }
1442
1518
  const indexes = extractIndexes(zodSchema) || [];
1519
+ if (isView && indexes.length > 0) {
1520
+ throw new Error(
1521
+ `View collection "${collectionName}" cannot declare indexes. PocketBase view collections are backed by a SQL query and do not support indexes.`
1522
+ );
1523
+ }
1443
1524
  const permissionAnalyzer = new PermissionAnalyzer();
1444
1525
  let permissions = void 0;
1445
1526
  const schemaDescription = zodSchema.description;
1446
1527
  const extractedPermissions = permissionAnalyzer.extractPermissions(schemaDescription);
1447
1528
  if (extractedPermissions) {
1448
1529
  const resolvedPermissions = permissionAnalyzer.resolvePermissions(extractedPermissions);
1449
- const validationResults = permissionAnalyzer.validatePermissions(
1450
- collectionName,
1451
- resolvedPermissions,
1452
- fields,
1453
- collectionType === "auth"
1454
- );
1455
- for (const [ruleType, result] of validationResults) {
1456
- if (!result.valid) {
1457
- console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1458
- result.errors.forEach((error) => console.error(` - ${error}`));
1530
+ if (isView) {
1531
+ for (const ruleType of ["createRule", "updateRule", "deleteRule", "manageRule"]) {
1532
+ if (resolvedPermissions[ruleType]) {
1533
+ console.warn(
1534
+ `[${collectionName}] ${ruleType} is not supported on view collections and will be set to null.`
1535
+ );
1536
+ }
1537
+ resolvedPermissions[ruleType] = null;
1459
1538
  }
1460
- if (result.warnings.length > 0) {
1461
- console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1462
- result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1539
+ }
1540
+ if (!isView) {
1541
+ const validationResults = permissionAnalyzer.validatePermissions(
1542
+ collectionName,
1543
+ resolvedPermissions,
1544
+ fields,
1545
+ collectionType === "auth"
1546
+ );
1547
+ for (const [ruleType, result] of validationResults) {
1548
+ if (!result.valid) {
1549
+ console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1550
+ result.errors.forEach((error) => console.error(` - ${error}`));
1551
+ }
1552
+ if (result.warnings.length > 0) {
1553
+ console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1554
+ result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1555
+ }
1463
1556
  }
1464
1557
  }
1465
1558
  permissions = permissionAnalyzer.mergeWithDefaults(resolvedPermissions);
@@ -1480,6 +1573,9 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1480
1573
  },
1481
1574
  permissions
1482
1575
  };
1576
+ if (isView) {
1577
+ collectionSchema.viewQuery = viewQuery;
1578
+ }
1483
1579
  return collectionSchema;
1484
1580
  }
1485
1581
  var tsxLoaderRegistered = false;
@@ -1737,8 +1833,9 @@ function convertPocketBaseField(pbField) {
1737
1833
  }
1738
1834
  function convertPocketBaseCollection(pbCollection) {
1739
1835
  const fields = [];
1836
+ const isView = pbCollection.type === "view";
1740
1837
  const systemFieldNames = ["id", "created", "updated", "collectionId", "collectionName", "expand"];
1741
- if (pbCollection.fields && Array.isArray(pbCollection.fields)) {
1838
+ if (!isView && pbCollection.fields && Array.isArray(pbCollection.fields)) {
1742
1839
  for (const pbField of pbCollection.fields) {
1743
1840
  if (pbField.system || systemFieldNames.includes(pbField.name)) {
1744
1841
  const isAuthSystemField = pbCollection.type === "auth" && ["email", "emailVisibility", "verified", "password", "tokenKey"].includes(pbField.name);
@@ -1758,6 +1855,9 @@ function convertPocketBaseCollection(pbCollection) {
1758
1855
  if (pbCollection.id) {
1759
1856
  schema.id = pbCollection.id;
1760
1857
  }
1858
+ if (typeof pbCollection.viewQuery === "string") {
1859
+ schema.viewQuery = dedentSql(pbCollection.viewQuery);
1860
+ }
1761
1861
  if (pbCollection.indexes && Array.isArray(pbCollection.indexes)) {
1762
1862
  schema.indexes = pbCollection.indexes;
1763
1863
  }
@@ -1859,6 +1959,24 @@ function findMigrationsAfterSnapshot(migrationsPath, snapshotTimestamp) {
1859
1959
  return [];
1860
1960
  }
1861
1961
  }
1962
+ function skipTemplateLiteral(content, start) {
1963
+ let i = start + 1;
1964
+ while (i < content.length) {
1965
+ const char = content[i];
1966
+ if (char === "\\") {
1967
+ i += 2;
1968
+ continue;
1969
+ }
1970
+ if (char === "`") {
1971
+ return i + 1;
1972
+ }
1973
+ i++;
1974
+ }
1975
+ return i;
1976
+ }
1977
+ function unescapeTemplateLiteral(raw) {
1978
+ return raw.replace(/\\(`|\$\{|\\)/g, "$1");
1979
+ }
1862
1980
  function parseMigrationOperationsFromContent(content) {
1863
1981
  const collectionsToCreate = [];
1864
1982
  const collectionsToDelete = [];
@@ -1947,6 +2065,10 @@ function parseMigrationOperationsFromContent(content) {
1947
2065
  while (i < content.length && bCount > 0) {
1948
2066
  const char = content[i];
1949
2067
  const prev = i > 0 ? content[i - 1] : "";
2068
+ if (!inStr && char === "`") {
2069
+ i = skipTemplateLiteral(content, i);
2070
+ continue;
2071
+ }
1950
2072
  if (!inStr && (char === '"' || char === "'")) {
1951
2073
  inStr = true;
1952
2074
  strChar = char;
@@ -2136,6 +2258,95 @@ function parseMigrationOperationsFromContent(content) {
2136
2258
  }
2137
2259
  }
2138
2260
  }
2261
+ const viewQueryRegex = /(\w+)\.viewQuery\s*=\s*/g;
2262
+ let viewQueryMatch;
2263
+ while ((viewQueryMatch = viewQueryRegex.exec(content)) !== null) {
2264
+ const varInfo = variables.get(viewQueryMatch[1]);
2265
+ if (!varInfo || varInfo.type !== "collection") continue;
2266
+ const valueStart = viewQueryMatch.index + viewQueryMatch[0].length;
2267
+ if (content[valueStart] === "`") {
2268
+ const end = skipTemplateLiteral(content, valueStart);
2269
+ const raw = content.substring(valueStart + 1, end - 1);
2270
+ getUpdate(varInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2271
+ viewQueryRegex.lastIndex = end;
2272
+ } else {
2273
+ const terminator = content.indexOf(";", valueStart);
2274
+ const valueStr = content.substring(valueStart, terminator === -1 ? content.length : terminator);
2275
+ try {
2276
+ getUpdate(varInfo.name).viewQuery = dedentSql(new Function("app", `return ${valueStr}`)(mockApp));
2277
+ } catch {
2278
+ getUpdate(varInfo.name).viewQuery = dedentSql(valueStr.trim());
2279
+ }
2280
+ }
2281
+ }
2282
+ const unmarshalRegex = /unmarshal\s*\(/g;
2283
+ let unmarshalMatch;
2284
+ while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
2285
+ let i = unmarshalMatch.index + unmarshalMatch[0].length;
2286
+ while (i < content.length && /\s/.test(content[i])) i++;
2287
+ if (content[i] !== "{") continue;
2288
+ const objStart = i;
2289
+ let braceCount = 1;
2290
+ let inStr = false;
2291
+ let strChar = null;
2292
+ i++;
2293
+ while (i < content.length && braceCount > 0) {
2294
+ const ch = content[i];
2295
+ const prev = i > 0 ? content[i - 1] : "";
2296
+ if (!inStr && ch === "`") {
2297
+ i = skipTemplateLiteral(content, i);
2298
+ continue;
2299
+ }
2300
+ if (!inStr && (ch === '"' || ch === "'")) {
2301
+ inStr = true;
2302
+ strChar = ch;
2303
+ } else if (inStr && ch === strChar && prev !== "\\") {
2304
+ inStr = false;
2305
+ strChar = null;
2306
+ }
2307
+ if (!inStr) {
2308
+ if (ch === "{") braceCount++;
2309
+ if (ch === "}") braceCount--;
2310
+ }
2311
+ i++;
2312
+ }
2313
+ if (braceCount !== 0) continue;
2314
+ const objStr = content.substring(objStart, i);
2315
+ while (i < content.length && /[\s,]/.test(content[i])) i++;
2316
+ const varStart = i;
2317
+ while (i < content.length && /\w/.test(content[i])) i++;
2318
+ const colVarName = content.substring(varStart, i);
2319
+ const colInfo = variables.get(colVarName);
2320
+ if (!colInfo || colInfo.type !== "collection") continue;
2321
+ const keyValRegex = /"(\w+Rule)"\s*:\s*([^,}\n]+)/g;
2322
+ let kvMatch;
2323
+ while ((kvMatch = keyValRegex.exec(objStr)) !== null) {
2324
+ const ruleKey = kvMatch[1];
2325
+ const valStr = kvMatch[2].trim();
2326
+ try {
2327
+ const value = new Function("app", `return ${valStr}`)(mockApp);
2328
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = value;
2329
+ } catch {
2330
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2331
+ }
2332
+ }
2333
+ const viewQueryKey = objStr.match(/"viewQuery"\s*:\s*/);
2334
+ if (viewQueryKey) {
2335
+ const valueStart = viewQueryKey.index + viewQueryKey[0].length;
2336
+ if (objStr[valueStart] === "`") {
2337
+ const end = skipTemplateLiteral(objStr, valueStart);
2338
+ const raw = objStr.substring(valueStart + 1, end - 1);
2339
+ getUpdate(colInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2340
+ } else {
2341
+ const valStr = objStr.substring(valueStart).replace(/[,}\s]+$/, "");
2342
+ try {
2343
+ getUpdate(colInfo.name).viewQuery = dedentSql(new Function("app", `return ${valStr}`)(mockApp));
2344
+ } catch {
2345
+ getUpdate(colInfo.name).viewQuery = dedentSql(valStr);
2346
+ }
2347
+ }
2348
+ }
2349
+ }
2139
2350
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2140
2351
  let match;
2141
2352
  while ((match = idxPushRegex.exec(content)) !== null) {
@@ -2265,6 +2476,10 @@ function parseMigrationOperations(migrationContent) {
2265
2476
  while (i < migrationContent.length && braceCount > 0) {
2266
2477
  const char = migrationContent[i];
2267
2478
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2479
+ if (!inString && char === "`") {
2480
+ i = skipTemplateLiteral(migrationContent, i);
2481
+ continue;
2482
+ }
2268
2483
  if (!inString && (char === '"' || char === "'")) {
2269
2484
  inString = true;
2270
2485
  stringChar = char;
@@ -2372,6 +2587,9 @@ function applyMigrationOperations(snapshot, operations) {
2372
2587
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2373
2588
  }
2374
2589
  }
2590
+ if (update.viewQuery !== void 0) {
2591
+ collection.viewQuery = update.viewQuery;
2592
+ }
2375
2593
  if (Object.keys(update.rulesToUpdate).length > 0) {
2376
2594
  if (!collection.rules) collection.rules = {};
2377
2595
  if (!collection.permissions) collection.permissions = {};
@@ -2835,7 +3053,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2835
3053
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2836
3054
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2837
3055
  }
3056
+ function normalizeSql(query) {
3057
+ if (!query) {
3058
+ return "";
3059
+ }
3060
+ return query.split("\n").map((line) => line.trim()).join(" ").replace(/\s+/g, " ").trim();
3061
+ }
3062
+ function compareViewQuery(currentCollection, previousCollection) {
3063
+ const newValue = currentCollection.viewQuery;
3064
+ if (typeof newValue !== "string") {
3065
+ return void 0;
3066
+ }
3067
+ if (normalizeSql(newValue) === normalizeSql(previousCollection.viewQuery)) {
3068
+ return void 0;
3069
+ }
3070
+ return {
3071
+ oldValue: previousCollection.viewQuery ?? null,
3072
+ newValue
3073
+ };
3074
+ }
2838
3075
  function buildCollectionModification(currentCollection, previousCollection, config, collectionIdToName) {
3076
+ const isView = currentCollection.type === "view" || previousCollection.type === "view";
3077
+ if (isView) {
3078
+ return {
3079
+ collection: currentCollection.name,
3080
+ fieldsToAdd: [],
3081
+ fieldsToRemove: [],
3082
+ fieldsToModify: [],
3083
+ indexesToAdd: [],
3084
+ indexesToRemove: [],
3085
+ rulesToUpdate: compareRules(
3086
+ currentCollection.rules,
3087
+ previousCollection.rules,
3088
+ currentCollection.permissions,
3089
+ previousCollection.permissions
3090
+ ),
3091
+ permissionsToUpdate: comparePermissions(currentCollection.permissions, previousCollection.permissions),
3092
+ viewQueryUpdate: compareViewQuery(currentCollection, previousCollection)
3093
+ };
3094
+ }
2839
3095
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2840
3096
  currentCollection,
2841
3097
  previousCollection,
@@ -2867,7 +3123,11 @@ function categorizeChangesBySeverity(diff, _config) {
2867
3123
  const destructive = [];
2868
3124
  const nonDestructive = [];
2869
3125
  for (const collection of diff.collectionsToDelete) {
2870
- destructive.push(`Delete collection: ${collection.name}`);
3126
+ if (collection.type === "view") {
3127
+ nonDestructive.push(`Delete view collection: ${collection.name}`);
3128
+ } else {
3129
+ destructive.push(`Delete collection: ${collection.name}`);
3130
+ }
2871
3131
  }
2872
3132
  for (const collection of diff.collectionsToCreate) {
2873
3133
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2902,6 +3162,9 @@ function categorizeChangesBySeverity(diff, _config) {
2902
3162
  for (const rule of modification.rulesToUpdate) {
2903
3163
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2904
3164
  }
3165
+ if (modification.viewQueryUpdate) {
3166
+ nonDestructive.push(`Update view query: ${collectionName}`);
3167
+ }
2905
3168
  }
2906
3169
  return { destructive, nonDestructive };
2907
3170
  }
@@ -2930,12 +3193,11 @@ function filterDiff(diff, options) {
2930
3193
  });
2931
3194
  let collectionsToDelete = diff.collectionsToDelete;
2932
3195
  if (skipDestructive) {
2933
- collectionsToDelete = [];
2934
- } else {
2935
- collectionsToDelete = collectionsToDelete.filter((col) => {
2936
- return matchesPattern(col.name, patterns);
2937
- });
3196
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
2938
3197
  }
3198
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3199
+ return matchesPattern(col.name, patterns);
3200
+ });
2939
3201
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
2940
3202
  const collectionMatches = matchesPattern(mod.collection, patterns);
2941
3203
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -2960,6 +3222,7 @@ function filterDiff(diff, options) {
2960
3222
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
2961
3223
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
2962
3224
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3225
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
2963
3226
  return {
2964
3227
  ...mod,
2965
3228
  fieldsToAdd,
@@ -2968,10 +3231,11 @@ function filterDiff(diff, options) {
2968
3231
  indexesToAdd,
2969
3232
  indexesToRemove,
2970
3233
  rulesToUpdate,
2971
- permissionsToUpdate
3234
+ permissionsToUpdate,
3235
+ viewQueryUpdate
2972
3236
  };
2973
3237
  }).filter((mod) => {
2974
- return mod.fieldsToAdd.length > 0 || mod.fieldsToRemove.length > 0 || mod.fieldsToModify.length > 0 || mod.indexesToAdd.length > 0 || mod.indexesToRemove.length > 0 || mod.rulesToUpdate.length > 0 || mod.permissionsToUpdate.length > 0;
3238
+ return mod.fieldsToAdd.length > 0 || mod.fieldsToRemove.length > 0 || mod.fieldsToModify.length > 0 || mod.indexesToAdd.length > 0 || mod.indexesToRemove.length > 0 || mod.rulesToUpdate.length > 0 || mod.permissionsToUpdate.length > 0 || mod.viewQueryUpdate !== void 0;
2975
3239
  });
2976
3240
  return {
2977
3241
  ...diff,
@@ -2983,7 +3247,7 @@ function filterDiff(diff, options) {
2983
3247
 
2984
3248
  // src/migration/diff/index.ts
2985
3249
  function hasChanges(modification) {
2986
- return modification.fieldsToAdd.length > 0 || modification.fieldsToRemove.length > 0 || modification.fieldsToModify.length > 0 || modification.indexesToAdd.length > 0 || modification.indexesToRemove.length > 0 || modification.rulesToUpdate.length > 0 || modification.permissionsToUpdate.length > 0;
3250
+ return modification.fieldsToAdd.length > 0 || modification.fieldsToRemove.length > 0 || modification.fieldsToModify.length > 0 || modification.indexesToAdd.length > 0 || modification.indexesToRemove.length > 0 || modification.rulesToUpdate.length > 0 || modification.permissionsToUpdate.length > 0 || modification.viewQueryUpdate !== void 0;
2987
3251
  }
2988
3252
  function aggregateChanges(currentSchema, previousSnapshot, config) {
2989
3253
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3162,6 +3426,13 @@ function formatValue(value) {
3162
3426
  }
3163
3427
  return String(value);
3164
3428
  }
3429
+ function formatSqlTemplate(query, indent = " ") {
3430
+ const escaped = query.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
3431
+ const body = escaped.split("\n").map((line) => line.trim() === "" ? "" : `${indent}${line}`).join("\n");
3432
+ return `\`
3433
+ ${body}
3434
+ ${indent.slice(0, -2)}\``;
3435
+ }
3165
3436
  function getFieldConstructorName(fieldType) {
3166
3437
  const constructorMap = {
3167
3438
  text: "TextField",
@@ -3200,6 +3471,36 @@ function getSystemFields() {
3200
3471
  }
3201
3472
  ];
3202
3473
  }
3474
+ function getSystemTimestampFields() {
3475
+ return [
3476
+ {
3477
+ name: "created",
3478
+ id: "autodate2990389176",
3479
+ type: "autodate",
3480
+ required: false,
3481
+ options: {
3482
+ hidden: false,
3483
+ onCreate: true,
3484
+ onUpdate: false,
3485
+ presentable: false,
3486
+ system: true
3487
+ }
3488
+ },
3489
+ {
3490
+ name: "updated",
3491
+ id: "autodate3332085495",
3492
+ type: "autodate",
3493
+ required: false,
3494
+ options: {
3495
+ hidden: false,
3496
+ onCreate: true,
3497
+ onUpdate: true,
3498
+ presentable: false,
3499
+ system: true
3500
+ }
3501
+ }
3502
+ ];
3503
+ }
3203
3504
  function getAuthSystemFields() {
3204
3505
  return [
3205
3506
  {
@@ -3492,6 +3793,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3492
3793
  if (!rules) {
3493
3794
  return "";
3494
3795
  }
3796
+ if (collectionType === "view") {
3797
+ return [
3798
+ `"listRule": ${formatValue(rules.listRule ?? null)}`,
3799
+ `"viewRule": ${formatValue(rules.viewRule ?? null)}`,
3800
+ `"createRule": null`,
3801
+ `"updateRule": null`,
3802
+ `"deleteRule": null`
3803
+ ].join(",\n ");
3804
+ }
3495
3805
  const parts = [];
3496
3806
  if (rules.listRule !== void 0) {
3497
3807
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3517,6 +3827,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3517
3827
  if (!permissions) {
3518
3828
  return "";
3519
3829
  }
3830
+ if (collectionType === "view") {
3831
+ return [
3832
+ `"listRule": ${formatValue(permissions.listRule ?? null)}`,
3833
+ `"viewRule": ${formatValue(permissions.viewRule ?? null)}`,
3834
+ `"createRule": null`,
3835
+ `"updateRule": null`,
3836
+ `"deleteRule": null`
3837
+ ].join(",\n ");
3838
+ }
3520
3839
  const parts = [];
3521
3840
  if (permissions.listRule !== void 0) {
3522
3841
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3554,6 +3873,28 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3554
3873
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3555
3874
  return lines.join("\n");
3556
3875
  }
3876
+ function generateViewQueryUpdate(collectionName, viewQuery, varName, isLast = false, collectionIdMap) {
3877
+ const lines = [];
3878
+ const collectionVar = varName || `collection_${collectionName}_viewQuery`;
3879
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3880
+ lines.push(` unmarshal({`);
3881
+ lines.push(` "viewQuery": ${formatSqlTemplate(viewQuery, " ")},`);
3882
+ lines.push(` }, ${collectionVar})`);
3883
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3884
+ return lines.join("\n");
3885
+ }
3886
+ function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3887
+ const collectionVar = `collection_${collectionName}_${varSuffix}`;
3888
+ const lines = [];
3889
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3890
+ lines.push(` unmarshal({`);
3891
+ for (const entry of entries) {
3892
+ lines.push(` "${entry.ruleType}": ${formatValue(entry.value)},`);
3893
+ }
3894
+ lines.push(` }, ${collectionVar})`);
3895
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3896
+ return lines.join("\n");
3897
+ }
3557
3898
 
3558
3899
  // src/migration/generator/collections.ts
3559
3900
  function generateCollectionCreation(collection, varName = "collection", isLast = false, collectionIdMap) {
@@ -3572,16 +3913,24 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3572
3913
  } else if (rulesCode) {
3573
3914
  lines.push(` ${rulesCode},`);
3574
3915
  }
3916
+ if (collection.type === "view") {
3917
+ lines.push(` "viewQuery": ${formatSqlTemplate(collection.viewQuery ?? "")},`);
3918
+ lines.push(` });`);
3919
+ lines.push(``);
3920
+ lines.push(isLast ? ` return app.save(${varName});` : ` app.save(${varName});`);
3921
+ return lines.join("\n");
3922
+ }
3575
3923
  const systemFieldNames = ["created", "updated", "id"];
3576
3924
  if (collection.type === "auth") {
3577
3925
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
3578
3926
  }
3579
3927
  const userFields = collection.fields.filter((f) => !systemFieldNames.includes(f.name));
3580
- const allFields = [...getSystemFields().filter((f) => f.name === "id")];
3928
+ const allFields = [...getSystemFields()];
3581
3929
  if (collection.type === "auth") {
3582
3930
  allFields.push(...getAuthSystemFields());
3583
3931
  }
3584
3932
  allFields.push(...userFields);
3933
+ allFields.push(...getSystemTimestampFields());
3585
3934
  lines.push(` "fields": ${generateFieldsArray(allFields, collectionIdMap)},`);
3586
3935
  let allIndexes = [...collection.indexes || []];
3587
3936
  if (collection.type === "auth") {
@@ -3611,7 +3960,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3611
3960
  const modification = operation.modifications;
3612
3961
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3613
3962
  let operationCount = 0;
3614
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
3963
+ const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + (modification.viewQueryUpdate ? 1 : 0) + (modification.permissionsToUpdate.length > 0 ? 1 : modification.rulesToUpdate.length > 0 ? 1 : 0);
3964
+ if (modification.viewQueryUpdate) {
3965
+ operationCount++;
3966
+ const isLast = operationCount === totalOperations;
3967
+ lines.push(
3968
+ generateViewQueryUpdate(
3969
+ collectionName,
3970
+ modification.viewQueryUpdate.newValue,
3971
+ void 0,
3972
+ isLast,
3973
+ collectionIdMap
3974
+ )
3975
+ );
3976
+ if (!isLast) lines.push("");
3977
+ }
3615
3978
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3616
3979
  const field = modification.fieldsToAdd[i];
3617
3980
  operationCount++;
@@ -3651,23 +4014,29 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3651
4014
  if (!isLast) lines.push("");
3652
4015
  }
3653
4016
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3654
- for (const permission of modification.permissionsToUpdate) {
3655
- operationCount++;
4017
+ operationCount++;
4018
+ const isLast = operationCount === totalOperations;
4019
+ if (modification.permissionsToUpdate.length >= 2) {
4020
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.newValue }));
4021
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4022
+ } else {
4023
+ const permission = modification.permissionsToUpdate[0];
3656
4024
  const varName = `collection_${collectionName}_perm_${permission.ruleType}`;
3657
- const isLast = operationCount === totalOperations;
3658
- lines.push(
3659
- generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap)
3660
- );
3661
- if (!isLast) lines.push("");
4025
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap));
3662
4026
  }
4027
+ if (!isLast) lines.push("");
3663
4028
  } else if (modification.rulesToUpdate.length > 0) {
3664
- for (const rule of modification.rulesToUpdate) {
3665
- operationCount++;
4029
+ operationCount++;
4030
+ const isLast = operationCount === totalOperations;
4031
+ if (modification.rulesToUpdate.length >= 2) {
4032
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.newValue }));
4033
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4034
+ } else {
4035
+ const rule = modification.rulesToUpdate[0];
3666
4036
  const varName = `collection_${collectionName}_rule_${rule.ruleType}`;
3667
- const isLast = operationCount === totalOperations;
3668
4037
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.newValue, varName, isLast, collectionIdMap));
3669
- if (!isLast) lines.push("");
3670
4038
  }
4039
+ if (!isLast) lines.push("");
3671
4040
  }
3672
4041
  } else if (operation.type === "delete") {
3673
4042
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection.name;
@@ -3706,25 +4075,45 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3706
4075
  const modification = operation.modifications;
3707
4076
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3708
4077
  let operationCount = 0;
3709
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
4078
+ const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + (modification.viewQueryUpdate ? 1 : 0) + (modification.permissionsToUpdate.length > 0 ? 1 : modification.rulesToUpdate.length > 0 ? 1 : 0);
4079
+ if (modification.viewQueryUpdate) {
4080
+ operationCount++;
4081
+ const isLast = operationCount === totalOperations;
4082
+ lines.push(
4083
+ generateViewQueryUpdate(
4084
+ collectionName,
4085
+ modification.viewQueryUpdate.oldValue ?? "",
4086
+ `collection_${collectionName}_revert_viewQuery`,
4087
+ isLast,
4088
+ collectionIdMap
4089
+ )
4090
+ );
4091
+ if (!isLast) lines.push("");
4092
+ }
3710
4093
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3711
- for (const permission of modification.permissionsToUpdate) {
3712
- operationCount++;
4094
+ operationCount++;
4095
+ const isLast = operationCount === totalOperations;
4096
+ if (modification.permissionsToUpdate.length >= 2) {
4097
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.oldValue }));
4098
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4099
+ } else {
4100
+ const permission = modification.permissionsToUpdate[0];
3713
4101
  const varName = `collection_${collectionName}_revert_perm_${permission.ruleType}`;
3714
- const isLast = operationCount === totalOperations;
3715
- lines.push(
3716
- generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap)
3717
- );
3718
- if (!isLast) lines.push("");
4102
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap));
3719
4103
  }
4104
+ if (!isLast) lines.push("");
3720
4105
  } else if (modification.rulesToUpdate.length > 0) {
3721
- for (const rule of modification.rulesToUpdate) {
3722
- operationCount++;
4106
+ operationCount++;
4107
+ const isLast = operationCount === totalOperations;
4108
+ if (modification.rulesToUpdate.length >= 2) {
4109
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.oldValue }));
4110
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4111
+ } else {
4112
+ const rule = modification.rulesToUpdate[0];
3723
4113
  const varName = `collection_${collectionName}_revert_rule_${rule.ruleType}`;
3724
- const isLast = operationCount === totalOperations;
3725
4114
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.oldValue, varName, isLast, collectionIdMap));
3726
- if (!isLast) lines.push("");
3727
4115
  }
4116
+ if (!isLast) lines.push("");
3728
4117
  }
3729
4118
  for (let i = 0; i < modification.indexesToRemove.length; i++) {
3730
4119
  operationCount++;
@@ -3973,6 +4362,9 @@ function generate(diff, config) {
3973
4362
  function detectCollectionDeletions(diff) {
3974
4363
  const changes = [];
3975
4364
  for (const collection of diff.collectionsToDelete) {
4365
+ if (collection.type === "view") {
4366
+ continue;
4367
+ }
3976
4368
  changes.push({
3977
4369
  type: "collection_deletion" /* COLLECTION_DELETION */,
3978
4370
  description: `Delete collection: ${collection.name}`,
@@ -4386,7 +4778,11 @@ function formatChangeSummary(diff) {
4386
4778
  lines.push(chalk.green.bold(`\u2713 ${totalCollectionsToCreate} collection(s) to create:`));
4387
4779
  for (const collection of diff.collectionsToCreate) {
4388
4780
  lines.push(chalk.green(` + ${collection.name} (${collection.type})`));
4389
- lines.push(chalk.gray(` ${collection.fields.length} field(s)`));
4781
+ if (collection.type === "view") {
4782
+ lines.push(chalk.gray(` fields derived from the view query`));
4783
+ } else {
4784
+ lines.push(chalk.gray(` ${collection.fields.length} field(s)`));
4785
+ }
4390
4786
  }
4391
4787
  lines.push("");
4392
4788
  }
@@ -4431,6 +4827,9 @@ function formatChangeSummary(diff) {
4431
4827
  if (modification.rulesToUpdate.length > 0) {
4432
4828
  lines.push(chalk.yellow(` ~ ${modification.rulesToUpdate.length} rule(s) to update`));
4433
4829
  }
4830
+ if (modification.viewQueryUpdate) {
4831
+ lines.push(chalk.yellow(` ~ view query to update`));
4832
+ }
4434
4833
  lines.push("");
4435
4834
  }
4436
4835
  }