pocketbase-zod-schema 0.7.1 → 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 +7 -0
  2. package/dist/cli/index.cjs +323 -27
  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 +323 -27
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/migrate.cjs +329 -30
  9. package/dist/cli/migrate.cjs.map +1 -1
  10. package/dist/cli/migrate.js +329 -30
  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 +100 -2
  37. package/dist/migration/generator.cjs.map +1 -1
  38. package/dist/migration/generator.d.cts +32 -2
  39. package/dist/migration/generator.d.ts +32 -2
  40. package/dist/migration/generator.js +99 -3
  41. package/dist/migration/generator.js.map +1 -1
  42. package/dist/migration/index.cjs +349 -26
  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 +349 -26
  47. package/dist/migration/index.js.map +1 -1
  48. package/dist/migration/snapshot.cjs +99 -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 +99 -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 +393 -28
  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 +390 -29
  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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.2](https://github.com/dastron/pocketbase-zod-schema/compare/pocketbase-zod-schema-v0.7.1...pocketbase-zod-schema-v0.7.2) (2026-07-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add defineView for view collections ([a239a96](https://github.com/dastron/pocketbase-zod-schema/commit/a239a96b9d9e69019be3625db50dbb27919baaee))
9
+
3
10
  ## [0.7.1](https://github.com/dastron/pocketbase-zod-schema/compare/pocketbase-zod-schema-v0.7.0...pocketbase-zod-schema-v0.7.1) (2026-05-06)
4
11
 
5
12
 
@@ -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,27 @@ 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
+ }
2166
2309
  const unmarshalRegex = /unmarshal\s*\(/g;
2167
2310
  let unmarshalMatch;
2168
2311
  while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
@@ -2177,6 +2320,10 @@ function parseMigrationOperationsFromContent(content) {
2177
2320
  while (i < content.length && braceCount > 0) {
2178
2321
  const ch = content[i];
2179
2322
  const prev = i > 0 ? content[i - 1] : "";
2323
+ if (!inStr && ch === "`") {
2324
+ i = skipTemplateLiteral(content, i);
2325
+ continue;
2326
+ }
2180
2327
  if (!inStr && (ch === '"' || ch === "'")) {
2181
2328
  inStr = true;
2182
2329
  strChar = ch;
@@ -2210,6 +2357,22 @@ function parseMigrationOperationsFromContent(content) {
2210
2357
  getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2211
2358
  }
2212
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
+ }
2213
2376
  }
2214
2377
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2215
2378
  let match;
@@ -2340,6 +2503,10 @@ function parseMigrationOperations(migrationContent) {
2340
2503
  while (i < migrationContent.length && braceCount > 0) {
2341
2504
  const char = migrationContent[i];
2342
2505
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2506
+ if (!inString && char === "`") {
2507
+ i = skipTemplateLiteral(migrationContent, i);
2508
+ continue;
2509
+ }
2343
2510
  if (!inString && (char === '"' || char === "'")) {
2344
2511
  inString = true;
2345
2512
  stringChar = char;
@@ -2447,6 +2614,9 @@ function applyMigrationOperations(snapshot, operations) {
2447
2614
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2448
2615
  }
2449
2616
  }
2617
+ if (update.viewQuery !== void 0) {
2618
+ collection.viewQuery = update.viewQuery;
2619
+ }
2450
2620
  if (Object.keys(update.rulesToUpdate).length > 0) {
2451
2621
  if (!collection.rules) collection.rules = {};
2452
2622
  if (!collection.permissions) collection.permissions = {};
@@ -2910,7 +3080,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2910
3080
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2911
3081
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2912
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
+ }
2913
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
+ }
2914
3122
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2915
3123
  currentCollection,
2916
3124
  previousCollection,
@@ -2942,7 +3150,11 @@ function categorizeChangesBySeverity(diff, _config) {
2942
3150
  const destructive = [];
2943
3151
  const nonDestructive = [];
2944
3152
  for (const collection of diff.collectionsToDelete) {
2945
- 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
+ }
2946
3158
  }
2947
3159
  for (const collection of diff.collectionsToCreate) {
2948
3160
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2977,6 +3189,9 @@ function categorizeChangesBySeverity(diff, _config) {
2977
3189
  for (const rule of modification.rulesToUpdate) {
2978
3190
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2979
3191
  }
3192
+ if (modification.viewQueryUpdate) {
3193
+ nonDestructive.push(`Update view query: ${collectionName}`);
3194
+ }
2980
3195
  }
2981
3196
  return { destructive, nonDestructive };
2982
3197
  }
@@ -3005,12 +3220,11 @@ function filterDiff(diff, options) {
3005
3220
  });
3006
3221
  let collectionsToDelete = diff.collectionsToDelete;
3007
3222
  if (skipDestructive) {
3008
- collectionsToDelete = [];
3009
- } else {
3010
- collectionsToDelete = collectionsToDelete.filter((col) => {
3011
- return matchesPattern(col.name, patterns);
3012
- });
3223
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
3013
3224
  }
3225
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3226
+ return matchesPattern(col.name, patterns);
3227
+ });
3014
3228
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
3015
3229
  const collectionMatches = matchesPattern(mod.collection, patterns);
3016
3230
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -3035,6 +3249,7 @@ function filterDiff(diff, options) {
3035
3249
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
3036
3250
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
3037
3251
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3252
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
3038
3253
  return {
3039
3254
  ...mod,
3040
3255
  fieldsToAdd,
@@ -3043,10 +3258,11 @@ function filterDiff(diff, options) {
3043
3258
  indexesToAdd,
3044
3259
  indexesToRemove,
3045
3260
  rulesToUpdate,
3046
- permissionsToUpdate
3261
+ permissionsToUpdate,
3262
+ viewQueryUpdate
3047
3263
  };
3048
3264
  }).filter((mod) => {
3049
- 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;
3050
3266
  });
3051
3267
  return {
3052
3268
  ...diff,
@@ -3058,7 +3274,7 @@ function filterDiff(diff, options) {
3058
3274
 
3059
3275
  // src/migration/diff/index.ts
3060
3276
  function hasChanges(modification) {
3061
- 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;
3062
3278
  }
3063
3279
  function aggregateChanges(currentSchema, previousSnapshot, config) {
3064
3280
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3237,6 +3453,13 @@ function formatValue(value) {
3237
3453
  }
3238
3454
  return String(value);
3239
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
+ }
3240
3463
  function getFieldConstructorName(fieldType) {
3241
3464
  const constructorMap = {
3242
3465
  text: "TextField",
@@ -3597,6 +3820,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3597
3820
  if (!rules) {
3598
3821
  return "";
3599
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
+ }
3600
3832
  const parts = [];
3601
3833
  if (rules.listRule !== void 0) {
3602
3834
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3622,6 +3854,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3622
3854
  if (!permissions) {
3623
3855
  return "";
3624
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
+ }
3625
3866
  const parts = [];
3626
3867
  if (permissions.listRule !== void 0) {
3627
3868
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3659,6 +3900,16 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3659
3900
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3660
3901
  return lines.join("\n");
3661
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
+ }
3662
3913
  function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3663
3914
  const collectionVar = `collection_${collectionName}_${varSuffix}`;
3664
3915
  const lines = [];
@@ -3689,6 +3940,13 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3689
3940
  } else if (rulesCode) {
3690
3941
  lines.push(` ${rulesCode},`);
3691
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
+ }
3692
3950
  const systemFieldNames = ["created", "updated", "id"];
3693
3951
  if (collection.type === "auth") {
3694
3952
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
@@ -3729,7 +3987,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3729
3987
  const modification = operation.modifications;
3730
3988
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3731
3989
  let operationCount = 0;
3732
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + (modification.permissionsToUpdate.length > 0 ? 1 : modification.rulesToUpdate.length > 0 ? 1 : 0);
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
+ }
3733
4005
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3734
4006
  const field = modification.fieldsToAdd[i];
3735
4007
  operationCount++;
@@ -3830,7 +4102,21 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3830
4102
  const modification = operation.modifications;
3831
4103
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3832
4104
  let operationCount = 0;
3833
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + (modification.permissionsToUpdate.length > 0 ? 1 : modification.rulesToUpdate.length > 0 ? 1 : 0);
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
+ }
3834
4120
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3835
4121
  operationCount++;
3836
4122
  const isLast = operationCount === totalOperations;
@@ -4103,6 +4389,9 @@ function generate(diff, config) {
4103
4389
  function detectCollectionDeletions(diff) {
4104
4390
  const changes = [];
4105
4391
  for (const collection of diff.collectionsToDelete) {
4392
+ if (collection.type === "view") {
4393
+ continue;
4394
+ }
4106
4395
  changes.push({
4107
4396
  type: "collection_deletion" /* COLLECTION_DELETION */,
4108
4397
  description: `Delete collection: ${collection.name}`,
@@ -4516,7 +4805,11 @@ function formatChangeSummary(diff) {
4516
4805
  lines.push(chalk__default.default.green.bold(`\u2713 ${totalCollectionsToCreate} collection(s) to create:`));
4517
4806
  for (const collection of diff.collectionsToCreate) {
4518
4807
  lines.push(chalk__default.default.green(` + ${collection.name} (${collection.type})`));
4519
- 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
+ }
4520
4813
  }
4521
4814
  lines.push("");
4522
4815
  }
@@ -4561,6 +4854,9 @@ function formatChangeSummary(diff) {
4561
4854
  if (modification.rulesToUpdate.length > 0) {
4562
4855
  lines.push(chalk__default.default.yellow(` ~ ${modification.rulesToUpdate.length} rule(s) to update`));
4563
4856
  }
4857
+ if (modification.viewQueryUpdate) {
4858
+ lines.push(chalk__default.default.yellow(` ~ view query to update`));
4859
+ }
4564
4860
  lines.push("");
4565
4861
  }
4566
4862
  }