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
@@ -1220,6 +1220,60 @@ function unwrapZodType(zodType) {
1220
1220
  }
1221
1221
  return unwrapped;
1222
1222
  }
1223
+ function dedentSql(value) {
1224
+ const lines = value.replace(/\r\n/g, "\n").split("\n");
1225
+ while (lines.length > 0 && lines[0].trim() === "") {
1226
+ lines.shift();
1227
+ }
1228
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1229
+ lines.pop();
1230
+ }
1231
+ if (lines.length === 0) {
1232
+ return "";
1233
+ }
1234
+ let minIndent = Infinity;
1235
+ for (const line of lines) {
1236
+ if (line.trim() === "") continue;
1237
+ const indent = line.length - line.trimStart().length;
1238
+ if (indent < minIndent) {
1239
+ minIndent = indent;
1240
+ }
1241
+ }
1242
+ if (!Number.isFinite(minIndent) || minIndent === 0) {
1243
+ return lines.map((line) => line.trimEnd()).join("\n");
1244
+ }
1245
+ return lines.map((line) => line.slice(minIndent).trimEnd()).join("\n");
1246
+ }
1247
+ function stripLeadingComments(query) {
1248
+ let remaining = query.trim();
1249
+ while (remaining.length > 0) {
1250
+ if (remaining.startsWith("--")) {
1251
+ const newline = remaining.indexOf("\n");
1252
+ remaining = newline === -1 ? "" : remaining.slice(newline + 1).trim();
1253
+ continue;
1254
+ }
1255
+ if (remaining.startsWith("/*")) {
1256
+ const end = remaining.indexOf("*/");
1257
+ remaining = end === -1 ? "" : remaining.slice(end + 2).trim();
1258
+ continue;
1259
+ }
1260
+ break;
1261
+ }
1262
+ return remaining;
1263
+ }
1264
+ function validateViewQuery(collectionName, viewQuery) {
1265
+ if (typeof viewQuery !== "string" || viewQuery.trim() === "") {
1266
+ throw new Error(
1267
+ `View collection "${collectionName}" requires a non-empty viewQuery. Provide the SQL SELECT statement backing the view.`
1268
+ );
1269
+ }
1270
+ const body = stripLeadingComments(viewQuery);
1271
+ if (!/^(select|with)\b/i.test(body)) {
1272
+ throw new Error(
1273
+ `View collection "${collectionName}" has an invalid viewQuery: it must start with SELECT or WITH. Received: ${body.slice(0, 40)}${body.length > 40 ? "..." : ""}`
1274
+ );
1275
+ }
1276
+ }
1223
1277
  function getCollectionNameFromFile(filePath) {
1224
1278
  const filename = path8__namespace.basename(filePath).replace(/\.(ts|js)$/, "");
1225
1279
  return toCollectionName(filename);
@@ -1243,13 +1297,26 @@ function extractCollectionTypeFromSchema(zodSchema) {
1243
1297
  }
1244
1298
  try {
1245
1299
  const metadata = JSON.parse(zodSchema.description);
1246
- if (metadata.type === "base" || metadata.type === "auth") {
1300
+ if (metadata.type === "base" || metadata.type === "auth" || metadata.type === "view") {
1247
1301
  return metadata.type;
1248
1302
  }
1249
1303
  } catch {
1250
1304
  }
1251
1305
  return null;
1252
1306
  }
1307
+ function extractViewQueryFromSchema(zodSchema) {
1308
+ if (!zodSchema.description) {
1309
+ return null;
1310
+ }
1311
+ try {
1312
+ const metadata = JSON.parse(zodSchema.description);
1313
+ if (typeof metadata.viewQuery === "string") {
1314
+ return metadata.viewQuery;
1315
+ }
1316
+ } catch {
1317
+ }
1318
+ return null;
1319
+ }
1253
1320
  function extractSchemaDefinitions(module, patterns = ["Schema", "InputSchema", "Collection"]) {
1254
1321
  const result = {};
1255
1322
  if (module.default instanceof zod.z.ZodObject) {
@@ -1408,6 +1475,15 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1408
1475
  const rawFields = extractFieldDefinitions(zodSchema);
1409
1476
  const explicitType = extractCollectionTypeFromSchema(zodSchema);
1410
1477
  const collectionType = explicitType ?? (isAuthCollection(rawFields) ? "auth" : "base");
1478
+ const isView = collectionType === "view";
1479
+ const viewQuery = extractViewQueryFromSchema(zodSchema);
1480
+ if (isView) {
1481
+ validateViewQuery(collectionName, viewQuery);
1482
+ } else if (viewQuery !== null) {
1483
+ console.warn(
1484
+ `[${collectionName}] viewQuery is only used by view collections and will be ignored. Use defineView() or set type: "view" to define a view collection.`
1485
+ );
1486
+ }
1411
1487
  const fields = rawFields.filter((f) => !["created", "updated"].includes(f.name)).map(({ name, zodType }) => buildFieldDefinition(name, zodType));
1412
1488
  if (collectionType === "auth") {
1413
1489
  const authSystemFields = [
@@ -1467,26 +1543,43 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1467
1543
  }
1468
1544
  }
1469
1545
  const indexes = extractIndexes(zodSchema) || [];
1546
+ if (isView && indexes.length > 0) {
1547
+ throw new Error(
1548
+ `View collection "${collectionName}" cannot declare indexes. PocketBase view collections are backed by a SQL query and do not support indexes.`
1549
+ );
1550
+ }
1470
1551
  const permissionAnalyzer = new PermissionAnalyzer();
1471
1552
  let permissions = void 0;
1472
1553
  const schemaDescription = zodSchema.description;
1473
1554
  const extractedPermissions = permissionAnalyzer.extractPermissions(schemaDescription);
1474
1555
  if (extractedPermissions) {
1475
1556
  const resolvedPermissions = permissionAnalyzer.resolvePermissions(extractedPermissions);
1476
- const validationResults = permissionAnalyzer.validatePermissions(
1477
- collectionName,
1478
- resolvedPermissions,
1479
- fields,
1480
- collectionType === "auth"
1481
- );
1482
- for (const [ruleType, result] of validationResults) {
1483
- if (!result.valid) {
1484
- console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1485
- result.errors.forEach((error) => console.error(` - ${error}`));
1557
+ if (isView) {
1558
+ for (const ruleType of ["createRule", "updateRule", "deleteRule", "manageRule"]) {
1559
+ if (resolvedPermissions[ruleType]) {
1560
+ console.warn(
1561
+ `[${collectionName}] ${ruleType} is not supported on view collections and will be set to null.`
1562
+ );
1563
+ }
1564
+ resolvedPermissions[ruleType] = null;
1486
1565
  }
1487
- if (result.warnings.length > 0) {
1488
- console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1489
- result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1566
+ }
1567
+ if (!isView) {
1568
+ const validationResults = permissionAnalyzer.validatePermissions(
1569
+ collectionName,
1570
+ resolvedPermissions,
1571
+ fields,
1572
+ collectionType === "auth"
1573
+ );
1574
+ for (const [ruleType, result] of validationResults) {
1575
+ if (!result.valid) {
1576
+ console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1577
+ result.errors.forEach((error) => console.error(` - ${error}`));
1578
+ }
1579
+ if (result.warnings.length > 0) {
1580
+ console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1581
+ result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1582
+ }
1490
1583
  }
1491
1584
  }
1492
1585
  permissions = permissionAnalyzer.mergeWithDefaults(resolvedPermissions);
@@ -1507,6 +1600,9 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1507
1600
  },
1508
1601
  permissions
1509
1602
  };
1603
+ if (isView) {
1604
+ collectionSchema.viewQuery = viewQuery;
1605
+ }
1510
1606
  return collectionSchema;
1511
1607
  }
1512
1608
  var tsxLoaderRegistered = false;
@@ -1764,8 +1860,9 @@ function convertPocketBaseField(pbField) {
1764
1860
  }
1765
1861
  function convertPocketBaseCollection(pbCollection) {
1766
1862
  const fields = [];
1863
+ const isView = pbCollection.type === "view";
1767
1864
  const systemFieldNames = ["id", "created", "updated", "collectionId", "collectionName", "expand"];
1768
- if (pbCollection.fields && Array.isArray(pbCollection.fields)) {
1865
+ if (!isView && pbCollection.fields && Array.isArray(pbCollection.fields)) {
1769
1866
  for (const pbField of pbCollection.fields) {
1770
1867
  if (pbField.system || systemFieldNames.includes(pbField.name)) {
1771
1868
  const isAuthSystemField = pbCollection.type === "auth" && ["email", "emailVisibility", "verified", "password", "tokenKey"].includes(pbField.name);
@@ -1785,6 +1882,9 @@ function convertPocketBaseCollection(pbCollection) {
1785
1882
  if (pbCollection.id) {
1786
1883
  schema.id = pbCollection.id;
1787
1884
  }
1885
+ if (typeof pbCollection.viewQuery === "string") {
1886
+ schema.viewQuery = dedentSql(pbCollection.viewQuery);
1887
+ }
1788
1888
  if (pbCollection.indexes && Array.isArray(pbCollection.indexes)) {
1789
1889
  schema.indexes = pbCollection.indexes;
1790
1890
  }
@@ -1886,6 +1986,24 @@ function findMigrationsAfterSnapshot(migrationsPath, snapshotTimestamp) {
1886
1986
  return [];
1887
1987
  }
1888
1988
  }
1989
+ function skipTemplateLiteral(content, start) {
1990
+ let i = start + 1;
1991
+ while (i < content.length) {
1992
+ const char = content[i];
1993
+ if (char === "\\") {
1994
+ i += 2;
1995
+ continue;
1996
+ }
1997
+ if (char === "`") {
1998
+ return i + 1;
1999
+ }
2000
+ i++;
2001
+ }
2002
+ return i;
2003
+ }
2004
+ function unescapeTemplateLiteral(raw) {
2005
+ return raw.replace(/\\(`|\$\{|\\)/g, "$1");
2006
+ }
1889
2007
  function parseMigrationOperationsFromContent(content) {
1890
2008
  const collectionsToCreate = [];
1891
2009
  const collectionsToDelete = [];
@@ -1974,6 +2092,10 @@ function parseMigrationOperationsFromContent(content) {
1974
2092
  while (i < content.length && bCount > 0) {
1975
2093
  const char = content[i];
1976
2094
  const prev = i > 0 ? content[i - 1] : "";
2095
+ if (!inStr && char === "`") {
2096
+ i = skipTemplateLiteral(content, i);
2097
+ continue;
2098
+ }
1977
2099
  if (!inStr && (char === '"' || char === "'")) {
1978
2100
  inStr = true;
1979
2101
  strChar = char;
@@ -2163,6 +2285,95 @@ function parseMigrationOperationsFromContent(content) {
2163
2285
  }
2164
2286
  }
2165
2287
  }
2288
+ const viewQueryRegex = /(\w+)\.viewQuery\s*=\s*/g;
2289
+ let viewQueryMatch;
2290
+ while ((viewQueryMatch = viewQueryRegex.exec(content)) !== null) {
2291
+ const varInfo = variables.get(viewQueryMatch[1]);
2292
+ if (!varInfo || varInfo.type !== "collection") continue;
2293
+ const valueStart = viewQueryMatch.index + viewQueryMatch[0].length;
2294
+ if (content[valueStart] === "`") {
2295
+ const end = skipTemplateLiteral(content, valueStart);
2296
+ const raw = content.substring(valueStart + 1, end - 1);
2297
+ getUpdate(varInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2298
+ viewQueryRegex.lastIndex = end;
2299
+ } else {
2300
+ const terminator = content.indexOf(";", valueStart);
2301
+ const valueStr = content.substring(valueStart, terminator === -1 ? content.length : terminator);
2302
+ try {
2303
+ getUpdate(varInfo.name).viewQuery = dedentSql(new Function("app", `return ${valueStr}`)(mockApp));
2304
+ } catch {
2305
+ getUpdate(varInfo.name).viewQuery = dedentSql(valueStr.trim());
2306
+ }
2307
+ }
2308
+ }
2309
+ const unmarshalRegex = /unmarshal\s*\(/g;
2310
+ let unmarshalMatch;
2311
+ while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
2312
+ let i = unmarshalMatch.index + unmarshalMatch[0].length;
2313
+ while (i < content.length && /\s/.test(content[i])) i++;
2314
+ if (content[i] !== "{") continue;
2315
+ const objStart = i;
2316
+ let braceCount = 1;
2317
+ let inStr = false;
2318
+ let strChar = null;
2319
+ i++;
2320
+ while (i < content.length && braceCount > 0) {
2321
+ const ch = content[i];
2322
+ const prev = i > 0 ? content[i - 1] : "";
2323
+ if (!inStr && ch === "`") {
2324
+ i = skipTemplateLiteral(content, i);
2325
+ continue;
2326
+ }
2327
+ if (!inStr && (ch === '"' || ch === "'")) {
2328
+ inStr = true;
2329
+ strChar = ch;
2330
+ } else if (inStr && ch === strChar && prev !== "\\") {
2331
+ inStr = false;
2332
+ strChar = null;
2333
+ }
2334
+ if (!inStr) {
2335
+ if (ch === "{") braceCount++;
2336
+ if (ch === "}") braceCount--;
2337
+ }
2338
+ i++;
2339
+ }
2340
+ if (braceCount !== 0) continue;
2341
+ const objStr = content.substring(objStart, i);
2342
+ while (i < content.length && /[\s,]/.test(content[i])) i++;
2343
+ const varStart = i;
2344
+ while (i < content.length && /\w/.test(content[i])) i++;
2345
+ const colVarName = content.substring(varStart, i);
2346
+ const colInfo = variables.get(colVarName);
2347
+ if (!colInfo || colInfo.type !== "collection") continue;
2348
+ const keyValRegex = /"(\w+Rule)"\s*:\s*([^,}\n]+)/g;
2349
+ let kvMatch;
2350
+ while ((kvMatch = keyValRegex.exec(objStr)) !== null) {
2351
+ const ruleKey = kvMatch[1];
2352
+ const valStr = kvMatch[2].trim();
2353
+ try {
2354
+ const value = new Function("app", `return ${valStr}`)(mockApp);
2355
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = value;
2356
+ } catch {
2357
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2358
+ }
2359
+ }
2360
+ const viewQueryKey = objStr.match(/"viewQuery"\s*:\s*/);
2361
+ if (viewQueryKey) {
2362
+ const valueStart = viewQueryKey.index + viewQueryKey[0].length;
2363
+ if (objStr[valueStart] === "`") {
2364
+ const end = skipTemplateLiteral(objStr, valueStart);
2365
+ const raw = objStr.substring(valueStart + 1, end - 1);
2366
+ getUpdate(colInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2367
+ } else {
2368
+ const valStr = objStr.substring(valueStart).replace(/[,}\s]+$/, "");
2369
+ try {
2370
+ getUpdate(colInfo.name).viewQuery = dedentSql(new Function("app", `return ${valStr}`)(mockApp));
2371
+ } catch {
2372
+ getUpdate(colInfo.name).viewQuery = dedentSql(valStr);
2373
+ }
2374
+ }
2375
+ }
2376
+ }
2166
2377
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2167
2378
  let match;
2168
2379
  while ((match = idxPushRegex.exec(content)) !== null) {
@@ -2292,6 +2503,10 @@ function parseMigrationOperations(migrationContent) {
2292
2503
  while (i < migrationContent.length && braceCount > 0) {
2293
2504
  const char = migrationContent[i];
2294
2505
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2506
+ if (!inString && char === "`") {
2507
+ i = skipTemplateLiteral(migrationContent, i);
2508
+ continue;
2509
+ }
2295
2510
  if (!inString && (char === '"' || char === "'")) {
2296
2511
  inString = true;
2297
2512
  stringChar = char;
@@ -2399,6 +2614,9 @@ function applyMigrationOperations(snapshot, operations) {
2399
2614
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2400
2615
  }
2401
2616
  }
2617
+ if (update.viewQuery !== void 0) {
2618
+ collection.viewQuery = update.viewQuery;
2619
+ }
2402
2620
  if (Object.keys(update.rulesToUpdate).length > 0) {
2403
2621
  if (!collection.rules) collection.rules = {};
2404
2622
  if (!collection.permissions) collection.permissions = {};
@@ -2862,7 +3080,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2862
3080
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2863
3081
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2864
3082
  }
3083
+ function normalizeSql(query) {
3084
+ if (!query) {
3085
+ return "";
3086
+ }
3087
+ return query.split("\n").map((line) => line.trim()).join(" ").replace(/\s+/g, " ").trim();
3088
+ }
3089
+ function compareViewQuery(currentCollection, previousCollection) {
3090
+ const newValue = currentCollection.viewQuery;
3091
+ if (typeof newValue !== "string") {
3092
+ return void 0;
3093
+ }
3094
+ if (normalizeSql(newValue) === normalizeSql(previousCollection.viewQuery)) {
3095
+ return void 0;
3096
+ }
3097
+ return {
3098
+ oldValue: previousCollection.viewQuery ?? null,
3099
+ newValue
3100
+ };
3101
+ }
2865
3102
  function buildCollectionModification(currentCollection, previousCollection, config, collectionIdToName) {
3103
+ const isView = currentCollection.type === "view" || previousCollection.type === "view";
3104
+ if (isView) {
3105
+ return {
3106
+ collection: currentCollection.name,
3107
+ fieldsToAdd: [],
3108
+ fieldsToRemove: [],
3109
+ fieldsToModify: [],
3110
+ indexesToAdd: [],
3111
+ indexesToRemove: [],
3112
+ rulesToUpdate: compareRules(
3113
+ currentCollection.rules,
3114
+ previousCollection.rules,
3115
+ currentCollection.permissions,
3116
+ previousCollection.permissions
3117
+ ),
3118
+ permissionsToUpdate: comparePermissions(currentCollection.permissions, previousCollection.permissions),
3119
+ viewQueryUpdate: compareViewQuery(currentCollection, previousCollection)
3120
+ };
3121
+ }
2866
3122
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2867
3123
  currentCollection,
2868
3124
  previousCollection,
@@ -2894,7 +3150,11 @@ function categorizeChangesBySeverity(diff, _config) {
2894
3150
  const destructive = [];
2895
3151
  const nonDestructive = [];
2896
3152
  for (const collection of diff.collectionsToDelete) {
2897
- destructive.push(`Delete collection: ${collection.name}`);
3153
+ if (collection.type === "view") {
3154
+ nonDestructive.push(`Delete view collection: ${collection.name}`);
3155
+ } else {
3156
+ destructive.push(`Delete collection: ${collection.name}`);
3157
+ }
2898
3158
  }
2899
3159
  for (const collection of diff.collectionsToCreate) {
2900
3160
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2929,6 +3189,9 @@ function categorizeChangesBySeverity(diff, _config) {
2929
3189
  for (const rule of modification.rulesToUpdate) {
2930
3190
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2931
3191
  }
3192
+ if (modification.viewQueryUpdate) {
3193
+ nonDestructive.push(`Update view query: ${collectionName}`);
3194
+ }
2932
3195
  }
2933
3196
  return { destructive, nonDestructive };
2934
3197
  }
@@ -2957,12 +3220,11 @@ function filterDiff(diff, options) {
2957
3220
  });
2958
3221
  let collectionsToDelete = diff.collectionsToDelete;
2959
3222
  if (skipDestructive) {
2960
- collectionsToDelete = [];
2961
- } else {
2962
- collectionsToDelete = collectionsToDelete.filter((col) => {
2963
- return matchesPattern(col.name, patterns);
2964
- });
3223
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
2965
3224
  }
3225
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3226
+ return matchesPattern(col.name, patterns);
3227
+ });
2966
3228
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
2967
3229
  const collectionMatches = matchesPattern(mod.collection, patterns);
2968
3230
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -2987,6 +3249,7 @@ function filterDiff(diff, options) {
2987
3249
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
2988
3250
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
2989
3251
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3252
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
2990
3253
  return {
2991
3254
  ...mod,
2992
3255
  fieldsToAdd,
@@ -2995,10 +3258,11 @@ function filterDiff(diff, options) {
2995
3258
  indexesToAdd,
2996
3259
  indexesToRemove,
2997
3260
  rulesToUpdate,
2998
- permissionsToUpdate
3261
+ permissionsToUpdate,
3262
+ viewQueryUpdate
2999
3263
  };
3000
3264
  }).filter((mod) => {
3001
- 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;
3265
+ 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;
3002
3266
  });
3003
3267
  return {
3004
3268
  ...diff,
@@ -3010,7 +3274,7 @@ function filterDiff(diff, options) {
3010
3274
 
3011
3275
  // src/migration/diff/index.ts
3012
3276
  function hasChanges(modification) {
3013
- 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;
3277
+ 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;
3014
3278
  }
3015
3279
  function aggregateChanges(currentSchema, previousSnapshot, config) {
3016
3280
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3189,6 +3453,13 @@ function formatValue(value) {
3189
3453
  }
3190
3454
  return String(value);
3191
3455
  }
3456
+ function formatSqlTemplate(query, indent = " ") {
3457
+ const escaped = query.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
3458
+ const body = escaped.split("\n").map((line) => line.trim() === "" ? "" : `${indent}${line}`).join("\n");
3459
+ return `\`
3460
+ ${body}
3461
+ ${indent.slice(0, -2)}\``;
3462
+ }
3192
3463
  function getFieldConstructorName(fieldType) {
3193
3464
  const constructorMap = {
3194
3465
  text: "TextField",
@@ -3227,6 +3498,36 @@ function getSystemFields() {
3227
3498
  }
3228
3499
  ];
3229
3500
  }
3501
+ function getSystemTimestampFields() {
3502
+ return [
3503
+ {
3504
+ name: "created",
3505
+ id: "autodate2990389176",
3506
+ type: "autodate",
3507
+ required: false,
3508
+ options: {
3509
+ hidden: false,
3510
+ onCreate: true,
3511
+ onUpdate: false,
3512
+ presentable: false,
3513
+ system: true
3514
+ }
3515
+ },
3516
+ {
3517
+ name: "updated",
3518
+ id: "autodate3332085495",
3519
+ type: "autodate",
3520
+ required: false,
3521
+ options: {
3522
+ hidden: false,
3523
+ onCreate: true,
3524
+ onUpdate: true,
3525
+ presentable: false,
3526
+ system: true
3527
+ }
3528
+ }
3529
+ ];
3530
+ }
3230
3531
  function getAuthSystemFields() {
3231
3532
  return [
3232
3533
  {
@@ -3519,6 +3820,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3519
3820
  if (!rules) {
3520
3821
  return "";
3521
3822
  }
3823
+ if (collectionType === "view") {
3824
+ return [
3825
+ `"listRule": ${formatValue(rules.listRule ?? null)}`,
3826
+ `"viewRule": ${formatValue(rules.viewRule ?? null)}`,
3827
+ `"createRule": null`,
3828
+ `"updateRule": null`,
3829
+ `"deleteRule": null`
3830
+ ].join(",\n ");
3831
+ }
3522
3832
  const parts = [];
3523
3833
  if (rules.listRule !== void 0) {
3524
3834
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3544,6 +3854,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3544
3854
  if (!permissions) {
3545
3855
  return "";
3546
3856
  }
3857
+ if (collectionType === "view") {
3858
+ return [
3859
+ `"listRule": ${formatValue(permissions.listRule ?? null)}`,
3860
+ `"viewRule": ${formatValue(permissions.viewRule ?? null)}`,
3861
+ `"createRule": null`,
3862
+ `"updateRule": null`,
3863
+ `"deleteRule": null`
3864
+ ].join(",\n ");
3865
+ }
3547
3866
  const parts = [];
3548
3867
  if (permissions.listRule !== void 0) {
3549
3868
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3581,6 +3900,28 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3581
3900
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3582
3901
  return lines.join("\n");
3583
3902
  }
3903
+ function generateViewQueryUpdate(collectionName, viewQuery, varName, isLast = false, collectionIdMap) {
3904
+ const lines = [];
3905
+ const collectionVar = varName || `collection_${collectionName}_viewQuery`;
3906
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3907
+ lines.push(` unmarshal({`);
3908
+ lines.push(` "viewQuery": ${formatSqlTemplate(viewQuery, " ")},`);
3909
+ lines.push(` }, ${collectionVar})`);
3910
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3911
+ return lines.join("\n");
3912
+ }
3913
+ function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3914
+ const collectionVar = `collection_${collectionName}_${varSuffix}`;
3915
+ const lines = [];
3916
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3917
+ lines.push(` unmarshal({`);
3918
+ for (const entry of entries) {
3919
+ lines.push(` "${entry.ruleType}": ${formatValue(entry.value)},`);
3920
+ }
3921
+ lines.push(` }, ${collectionVar})`);
3922
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3923
+ return lines.join("\n");
3924
+ }
3584
3925
 
3585
3926
  // src/migration/generator/collections.ts
3586
3927
  function generateCollectionCreation(collection, varName = "collection", isLast = false, collectionIdMap) {
@@ -3599,16 +3940,24 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3599
3940
  } else if (rulesCode) {
3600
3941
  lines.push(` ${rulesCode},`);
3601
3942
  }
3943
+ if (collection.type === "view") {
3944
+ lines.push(` "viewQuery": ${formatSqlTemplate(collection.viewQuery ?? "")},`);
3945
+ lines.push(` });`);
3946
+ lines.push(``);
3947
+ lines.push(isLast ? ` return app.save(${varName});` : ` app.save(${varName});`);
3948
+ return lines.join("\n");
3949
+ }
3602
3950
  const systemFieldNames = ["created", "updated", "id"];
3603
3951
  if (collection.type === "auth") {
3604
3952
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
3605
3953
  }
3606
3954
  const userFields = collection.fields.filter((f) => !systemFieldNames.includes(f.name));
3607
- const allFields = [...getSystemFields().filter((f) => f.name === "id")];
3955
+ const allFields = [...getSystemFields()];
3608
3956
  if (collection.type === "auth") {
3609
3957
  allFields.push(...getAuthSystemFields());
3610
3958
  }
3611
3959
  allFields.push(...userFields);
3960
+ allFields.push(...getSystemTimestampFields());
3612
3961
  lines.push(` "fields": ${generateFieldsArray(allFields, collectionIdMap)},`);
3613
3962
  let allIndexes = [...collection.indexes || []];
3614
3963
  if (collection.type === "auth") {
@@ -3638,7 +3987,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3638
3987
  const modification = operation.modifications;
3639
3988
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3640
3989
  let operationCount = 0;
3641
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
3990
+ 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);
3991
+ if (modification.viewQueryUpdate) {
3992
+ operationCount++;
3993
+ const isLast = operationCount === totalOperations;
3994
+ lines.push(
3995
+ generateViewQueryUpdate(
3996
+ collectionName,
3997
+ modification.viewQueryUpdate.newValue,
3998
+ void 0,
3999
+ isLast,
4000
+ collectionIdMap
4001
+ )
4002
+ );
4003
+ if (!isLast) lines.push("");
4004
+ }
3642
4005
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3643
4006
  const field = modification.fieldsToAdd[i];
3644
4007
  operationCount++;
@@ -3678,23 +4041,29 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3678
4041
  if (!isLast) lines.push("");
3679
4042
  }
3680
4043
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3681
- for (const permission of modification.permissionsToUpdate) {
3682
- operationCount++;
4044
+ operationCount++;
4045
+ const isLast = operationCount === totalOperations;
4046
+ if (modification.permissionsToUpdate.length >= 2) {
4047
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.newValue }));
4048
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4049
+ } else {
4050
+ const permission = modification.permissionsToUpdate[0];
3683
4051
  const varName = `collection_${collectionName}_perm_${permission.ruleType}`;
3684
- const isLast = operationCount === totalOperations;
3685
- lines.push(
3686
- generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap)
3687
- );
3688
- if (!isLast) lines.push("");
4052
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap));
3689
4053
  }
4054
+ if (!isLast) lines.push("");
3690
4055
  } else if (modification.rulesToUpdate.length > 0) {
3691
- for (const rule of modification.rulesToUpdate) {
3692
- operationCount++;
4056
+ operationCount++;
4057
+ const isLast = operationCount === totalOperations;
4058
+ if (modification.rulesToUpdate.length >= 2) {
4059
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.newValue }));
4060
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4061
+ } else {
4062
+ const rule = modification.rulesToUpdate[0];
3693
4063
  const varName = `collection_${collectionName}_rule_${rule.ruleType}`;
3694
- const isLast = operationCount === totalOperations;
3695
4064
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.newValue, varName, isLast, collectionIdMap));
3696
- if (!isLast) lines.push("");
3697
4065
  }
4066
+ if (!isLast) lines.push("");
3698
4067
  }
3699
4068
  } else if (operation.type === "delete") {
3700
4069
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection.name;
@@ -3733,25 +4102,45 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3733
4102
  const modification = operation.modifications;
3734
4103
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3735
4104
  let operationCount = 0;
3736
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
4105
+ 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);
4106
+ if (modification.viewQueryUpdate) {
4107
+ operationCount++;
4108
+ const isLast = operationCount === totalOperations;
4109
+ lines.push(
4110
+ generateViewQueryUpdate(
4111
+ collectionName,
4112
+ modification.viewQueryUpdate.oldValue ?? "",
4113
+ `collection_${collectionName}_revert_viewQuery`,
4114
+ isLast,
4115
+ collectionIdMap
4116
+ )
4117
+ );
4118
+ if (!isLast) lines.push("");
4119
+ }
3737
4120
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3738
- for (const permission of modification.permissionsToUpdate) {
3739
- operationCount++;
4121
+ operationCount++;
4122
+ const isLast = operationCount === totalOperations;
4123
+ if (modification.permissionsToUpdate.length >= 2) {
4124
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.oldValue }));
4125
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4126
+ } else {
4127
+ const permission = modification.permissionsToUpdate[0];
3740
4128
  const varName = `collection_${collectionName}_revert_perm_${permission.ruleType}`;
3741
- const isLast = operationCount === totalOperations;
3742
- lines.push(
3743
- generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap)
3744
- );
3745
- if (!isLast) lines.push("");
4129
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap));
3746
4130
  }
4131
+ if (!isLast) lines.push("");
3747
4132
  } else if (modification.rulesToUpdate.length > 0) {
3748
- for (const rule of modification.rulesToUpdate) {
3749
- operationCount++;
4133
+ operationCount++;
4134
+ const isLast = operationCount === totalOperations;
4135
+ if (modification.rulesToUpdate.length >= 2) {
4136
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.oldValue }));
4137
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4138
+ } else {
4139
+ const rule = modification.rulesToUpdate[0];
3750
4140
  const varName = `collection_${collectionName}_revert_rule_${rule.ruleType}`;
3751
- const isLast = operationCount === totalOperations;
3752
4141
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.oldValue, varName, isLast, collectionIdMap));
3753
- if (!isLast) lines.push("");
3754
4142
  }
4143
+ if (!isLast) lines.push("");
3755
4144
  }
3756
4145
  for (let i = 0; i < modification.indexesToRemove.length; i++) {
3757
4146
  operationCount++;
@@ -4000,6 +4389,9 @@ function generate(diff, config) {
4000
4389
  function detectCollectionDeletions(diff) {
4001
4390
  const changes = [];
4002
4391
  for (const collection of diff.collectionsToDelete) {
4392
+ if (collection.type === "view") {
4393
+ continue;
4394
+ }
4003
4395
  changes.push({
4004
4396
  type: "collection_deletion" /* COLLECTION_DELETION */,
4005
4397
  description: `Delete collection: ${collection.name}`,
@@ -4413,7 +4805,11 @@ function formatChangeSummary(diff) {
4413
4805
  lines.push(chalk__default.default.green.bold(`\u2713 ${totalCollectionsToCreate} collection(s) to create:`));
4414
4806
  for (const collection of diff.collectionsToCreate) {
4415
4807
  lines.push(chalk__default.default.green(` + ${collection.name} (${collection.type})`));
4416
- lines.push(chalk__default.default.gray(` ${collection.fields.length} field(s)`));
4808
+ if (collection.type === "view") {
4809
+ lines.push(chalk__default.default.gray(` fields derived from the view query`));
4810
+ } else {
4811
+ lines.push(chalk__default.default.gray(` ${collection.fields.length} field(s)`));
4812
+ }
4417
4813
  }
4418
4814
  lines.push("");
4419
4815
  }
@@ -4458,6 +4854,9 @@ function formatChangeSummary(diff) {
4458
4854
  if (modification.rulesToUpdate.length > 0) {
4459
4855
  lines.push(chalk__default.default.yellow(` ~ ${modification.rulesToUpdate.length} rule(s) to update`));
4460
4856
  }
4857
+ if (modification.viewQueryUpdate) {
4858
+ lines.push(chalk__default.default.yellow(` ~ view query to update`));
4859
+ }
4461
4860
  lines.push("");
4462
4861
  }
4463
4862
  }