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
@@ -1225,6 +1225,60 @@ function unwrapZodType(zodType) {
1225
1225
  }
1226
1226
  return unwrapped;
1227
1227
  }
1228
+ function dedentSql(value) {
1229
+ const lines = value.replace(/\r\n/g, "\n").split("\n");
1230
+ while (lines.length > 0 && lines[0].trim() === "") {
1231
+ lines.shift();
1232
+ }
1233
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1234
+ lines.pop();
1235
+ }
1236
+ if (lines.length === 0) {
1237
+ return "";
1238
+ }
1239
+ let minIndent = Infinity;
1240
+ for (const line of lines) {
1241
+ if (line.trim() === "") continue;
1242
+ const indent = line.length - line.trimStart().length;
1243
+ if (indent < minIndent) {
1244
+ minIndent = indent;
1245
+ }
1246
+ }
1247
+ if (!Number.isFinite(minIndent) || minIndent === 0) {
1248
+ return lines.map((line) => line.trimEnd()).join("\n");
1249
+ }
1250
+ return lines.map((line) => line.slice(minIndent).trimEnd()).join("\n");
1251
+ }
1252
+ function stripLeadingComments(query) {
1253
+ let remaining = query.trim();
1254
+ while (remaining.length > 0) {
1255
+ if (remaining.startsWith("--")) {
1256
+ const newline = remaining.indexOf("\n");
1257
+ remaining = newline === -1 ? "" : remaining.slice(newline + 1).trim();
1258
+ continue;
1259
+ }
1260
+ if (remaining.startsWith("/*")) {
1261
+ const end = remaining.indexOf("*/");
1262
+ remaining = end === -1 ? "" : remaining.slice(end + 2).trim();
1263
+ continue;
1264
+ }
1265
+ break;
1266
+ }
1267
+ return remaining;
1268
+ }
1269
+ function validateViewQuery(collectionName, viewQuery) {
1270
+ if (typeof viewQuery !== "string" || viewQuery.trim() === "") {
1271
+ throw new Error(
1272
+ `View collection "${collectionName}" requires a non-empty viewQuery. Provide the SQL SELECT statement backing the view.`
1273
+ );
1274
+ }
1275
+ const body = stripLeadingComments(viewQuery);
1276
+ if (!/^(select|with)\b/i.test(body)) {
1277
+ throw new Error(
1278
+ `View collection "${collectionName}" has an invalid viewQuery: it must start with SELECT or WITH. Received: ${body.slice(0, 40)}${body.length > 40 ? "..." : ""}`
1279
+ );
1280
+ }
1281
+ }
1228
1282
  function getCollectionNameFromFile(filePath) {
1229
1283
  const filename = path8__namespace.basename(filePath).replace(/\.(ts|js)$/, "");
1230
1284
  return toCollectionName(filename);
@@ -1248,13 +1302,26 @@ function extractCollectionTypeFromSchema(zodSchema) {
1248
1302
  }
1249
1303
  try {
1250
1304
  const metadata = JSON.parse(zodSchema.description);
1251
- if (metadata.type === "base" || metadata.type === "auth") {
1305
+ if (metadata.type === "base" || metadata.type === "auth" || metadata.type === "view") {
1252
1306
  return metadata.type;
1253
1307
  }
1254
1308
  } catch {
1255
1309
  }
1256
1310
  return null;
1257
1311
  }
1312
+ function extractViewQueryFromSchema(zodSchema) {
1313
+ if (!zodSchema.description) {
1314
+ return null;
1315
+ }
1316
+ try {
1317
+ const metadata = JSON.parse(zodSchema.description);
1318
+ if (typeof metadata.viewQuery === "string") {
1319
+ return metadata.viewQuery;
1320
+ }
1321
+ } catch {
1322
+ }
1323
+ return null;
1324
+ }
1258
1325
  function extractSchemaDefinitions(module, patterns = ["Schema", "InputSchema", "Collection"]) {
1259
1326
  const result = {};
1260
1327
  if (module.default instanceof zod.z.ZodObject) {
@@ -1413,6 +1480,15 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1413
1480
  const rawFields = extractFieldDefinitions(zodSchema);
1414
1481
  const explicitType = extractCollectionTypeFromSchema(zodSchema);
1415
1482
  const collectionType = explicitType ?? (isAuthCollection(rawFields) ? "auth" : "base");
1483
+ const isView = collectionType === "view";
1484
+ const viewQuery = extractViewQueryFromSchema(zodSchema);
1485
+ if (isView) {
1486
+ validateViewQuery(collectionName, viewQuery);
1487
+ } else if (viewQuery !== null) {
1488
+ console.warn(
1489
+ `[${collectionName}] viewQuery is only used by view collections and will be ignored. Use defineView() or set type: "view" to define a view collection.`
1490
+ );
1491
+ }
1416
1492
  const fields = rawFields.filter((f) => !["created", "updated"].includes(f.name)).map(({ name, zodType }) => buildFieldDefinition(name, zodType));
1417
1493
  if (collectionType === "auth") {
1418
1494
  const authSystemFields = [
@@ -1472,26 +1548,43 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1472
1548
  }
1473
1549
  }
1474
1550
  const indexes = extractIndexes(zodSchema) || [];
1551
+ if (isView && indexes.length > 0) {
1552
+ throw new Error(
1553
+ `View collection "${collectionName}" cannot declare indexes. PocketBase view collections are backed by a SQL query and do not support indexes.`
1554
+ );
1555
+ }
1475
1556
  const permissionAnalyzer = new PermissionAnalyzer();
1476
1557
  let permissions = void 0;
1477
1558
  const schemaDescription = zodSchema.description;
1478
1559
  const extractedPermissions = permissionAnalyzer.extractPermissions(schemaDescription);
1479
1560
  if (extractedPermissions) {
1480
1561
  const resolvedPermissions = permissionAnalyzer.resolvePermissions(extractedPermissions);
1481
- const validationResults = permissionAnalyzer.validatePermissions(
1482
- collectionName,
1483
- resolvedPermissions,
1484
- fields,
1485
- collectionType === "auth"
1486
- );
1487
- for (const [ruleType, result] of validationResults) {
1488
- if (!result.valid) {
1489
- console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1490
- result.errors.forEach((error) => console.error(` - ${error}`));
1562
+ if (isView) {
1563
+ for (const ruleType of ["createRule", "updateRule", "deleteRule", "manageRule"]) {
1564
+ if (resolvedPermissions[ruleType]) {
1565
+ console.warn(
1566
+ `[${collectionName}] ${ruleType} is not supported on view collections and will be set to null.`
1567
+ );
1568
+ }
1569
+ resolvedPermissions[ruleType] = null;
1491
1570
  }
1492
- if (result.warnings.length > 0) {
1493
- console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1494
- result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1571
+ }
1572
+ if (!isView) {
1573
+ const validationResults = permissionAnalyzer.validatePermissions(
1574
+ collectionName,
1575
+ resolvedPermissions,
1576
+ fields,
1577
+ collectionType === "auth"
1578
+ );
1579
+ for (const [ruleType, result] of validationResults) {
1580
+ if (!result.valid) {
1581
+ console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1582
+ result.errors.forEach((error) => console.error(` - ${error}`));
1583
+ }
1584
+ if (result.warnings.length > 0) {
1585
+ console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1586
+ result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1587
+ }
1495
1588
  }
1496
1589
  }
1497
1590
  permissions = permissionAnalyzer.mergeWithDefaults(resolvedPermissions);
@@ -1512,6 +1605,9 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1512
1605
  },
1513
1606
  permissions
1514
1607
  };
1608
+ if (isView) {
1609
+ collectionSchema.viewQuery = viewQuery;
1610
+ }
1515
1611
  return collectionSchema;
1516
1612
  }
1517
1613
  var tsxLoaderRegistered = false;
@@ -1769,8 +1865,9 @@ function convertPocketBaseField(pbField) {
1769
1865
  }
1770
1866
  function convertPocketBaseCollection(pbCollection) {
1771
1867
  const fields = [];
1868
+ const isView = pbCollection.type === "view";
1772
1869
  const systemFieldNames = ["id", "created", "updated", "collectionId", "collectionName", "expand"];
1773
- if (pbCollection.fields && Array.isArray(pbCollection.fields)) {
1870
+ if (!isView && pbCollection.fields && Array.isArray(pbCollection.fields)) {
1774
1871
  for (const pbField of pbCollection.fields) {
1775
1872
  if (pbField.system || systemFieldNames.includes(pbField.name)) {
1776
1873
  const isAuthSystemField = pbCollection.type === "auth" && ["email", "emailVisibility", "verified", "password", "tokenKey"].includes(pbField.name);
@@ -1790,6 +1887,9 @@ function convertPocketBaseCollection(pbCollection) {
1790
1887
  if (pbCollection.id) {
1791
1888
  schema.id = pbCollection.id;
1792
1889
  }
1890
+ if (typeof pbCollection.viewQuery === "string") {
1891
+ schema.viewQuery = dedentSql(pbCollection.viewQuery);
1892
+ }
1793
1893
  if (pbCollection.indexes && Array.isArray(pbCollection.indexes)) {
1794
1894
  schema.indexes = pbCollection.indexes;
1795
1895
  }
@@ -1891,6 +1991,24 @@ function findMigrationsAfterSnapshot(migrationsPath, snapshotTimestamp) {
1891
1991
  return [];
1892
1992
  }
1893
1993
  }
1994
+ function skipTemplateLiteral(content, start) {
1995
+ let i = start + 1;
1996
+ while (i < content.length) {
1997
+ const char = content[i];
1998
+ if (char === "\\") {
1999
+ i += 2;
2000
+ continue;
2001
+ }
2002
+ if (char === "`") {
2003
+ return i + 1;
2004
+ }
2005
+ i++;
2006
+ }
2007
+ return i;
2008
+ }
2009
+ function unescapeTemplateLiteral(raw) {
2010
+ return raw.replace(/\\(`|\$\{|\\)/g, "$1");
2011
+ }
1894
2012
  function parseMigrationOperationsFromContent(content) {
1895
2013
  const collectionsToCreate = [];
1896
2014
  const collectionsToDelete = [];
@@ -1979,6 +2097,10 @@ function parseMigrationOperationsFromContent(content) {
1979
2097
  while (i < content.length && bCount > 0) {
1980
2098
  const char = content[i];
1981
2099
  const prev = i > 0 ? content[i - 1] : "";
2100
+ if (!inStr && char === "`") {
2101
+ i = skipTemplateLiteral(content, i);
2102
+ continue;
2103
+ }
1982
2104
  if (!inStr && (char === '"' || char === "'")) {
1983
2105
  inStr = true;
1984
2106
  strChar = char;
@@ -2168,6 +2290,95 @@ function parseMigrationOperationsFromContent(content) {
2168
2290
  }
2169
2291
  }
2170
2292
  }
2293
+ const viewQueryRegex = /(\w+)\.viewQuery\s*=\s*/g;
2294
+ let viewQueryMatch;
2295
+ while ((viewQueryMatch = viewQueryRegex.exec(content)) !== null) {
2296
+ const varInfo = variables.get(viewQueryMatch[1]);
2297
+ if (!varInfo || varInfo.type !== "collection") continue;
2298
+ const valueStart = viewQueryMatch.index + viewQueryMatch[0].length;
2299
+ if (content[valueStart] === "`") {
2300
+ const end = skipTemplateLiteral(content, valueStart);
2301
+ const raw = content.substring(valueStart + 1, end - 1);
2302
+ getUpdate(varInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2303
+ viewQueryRegex.lastIndex = end;
2304
+ } else {
2305
+ const terminator = content.indexOf(";", valueStart);
2306
+ const valueStr = content.substring(valueStart, terminator === -1 ? content.length : terminator);
2307
+ try {
2308
+ getUpdate(varInfo.name).viewQuery = dedentSql(new Function("app", `return ${valueStr}`)(mockApp));
2309
+ } catch {
2310
+ getUpdate(varInfo.name).viewQuery = dedentSql(valueStr.trim());
2311
+ }
2312
+ }
2313
+ }
2314
+ const unmarshalRegex = /unmarshal\s*\(/g;
2315
+ let unmarshalMatch;
2316
+ while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
2317
+ let i = unmarshalMatch.index + unmarshalMatch[0].length;
2318
+ while (i < content.length && /\s/.test(content[i])) i++;
2319
+ if (content[i] !== "{") continue;
2320
+ const objStart = i;
2321
+ let braceCount = 1;
2322
+ let inStr = false;
2323
+ let strChar = null;
2324
+ i++;
2325
+ while (i < content.length && braceCount > 0) {
2326
+ const ch = content[i];
2327
+ const prev = i > 0 ? content[i - 1] : "";
2328
+ if (!inStr && ch === "`") {
2329
+ i = skipTemplateLiteral(content, i);
2330
+ continue;
2331
+ }
2332
+ if (!inStr && (ch === '"' || ch === "'")) {
2333
+ inStr = true;
2334
+ strChar = ch;
2335
+ } else if (inStr && ch === strChar && prev !== "\\") {
2336
+ inStr = false;
2337
+ strChar = null;
2338
+ }
2339
+ if (!inStr) {
2340
+ if (ch === "{") braceCount++;
2341
+ if (ch === "}") braceCount--;
2342
+ }
2343
+ i++;
2344
+ }
2345
+ if (braceCount !== 0) continue;
2346
+ const objStr = content.substring(objStart, i);
2347
+ while (i < content.length && /[\s,]/.test(content[i])) i++;
2348
+ const varStart = i;
2349
+ while (i < content.length && /\w/.test(content[i])) i++;
2350
+ const colVarName = content.substring(varStart, i);
2351
+ const colInfo = variables.get(colVarName);
2352
+ if (!colInfo || colInfo.type !== "collection") continue;
2353
+ const keyValRegex = /"(\w+Rule)"\s*:\s*([^,}\n]+)/g;
2354
+ let kvMatch;
2355
+ while ((kvMatch = keyValRegex.exec(objStr)) !== null) {
2356
+ const ruleKey = kvMatch[1];
2357
+ const valStr = kvMatch[2].trim();
2358
+ try {
2359
+ const value = new Function("app", `return ${valStr}`)(mockApp);
2360
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = value;
2361
+ } catch {
2362
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2363
+ }
2364
+ }
2365
+ const viewQueryKey = objStr.match(/"viewQuery"\s*:\s*/);
2366
+ if (viewQueryKey) {
2367
+ const valueStart = viewQueryKey.index + viewQueryKey[0].length;
2368
+ if (objStr[valueStart] === "`") {
2369
+ const end = skipTemplateLiteral(objStr, valueStart);
2370
+ const raw = objStr.substring(valueStart + 1, end - 1);
2371
+ getUpdate(colInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2372
+ } else {
2373
+ const valStr = objStr.substring(valueStart).replace(/[,}\s]+$/, "");
2374
+ try {
2375
+ getUpdate(colInfo.name).viewQuery = dedentSql(new Function("app", `return ${valStr}`)(mockApp));
2376
+ } catch {
2377
+ getUpdate(colInfo.name).viewQuery = dedentSql(valStr);
2378
+ }
2379
+ }
2380
+ }
2381
+ }
2171
2382
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2172
2383
  let match;
2173
2384
  while ((match = idxPushRegex.exec(content)) !== null) {
@@ -2297,6 +2508,10 @@ function parseMigrationOperations(migrationContent) {
2297
2508
  while (i < migrationContent.length && braceCount > 0) {
2298
2509
  const char = migrationContent[i];
2299
2510
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2511
+ if (!inString && char === "`") {
2512
+ i = skipTemplateLiteral(migrationContent, i);
2513
+ continue;
2514
+ }
2300
2515
  if (!inString && (char === '"' || char === "'")) {
2301
2516
  inString = true;
2302
2517
  stringChar = char;
@@ -2404,6 +2619,9 @@ function applyMigrationOperations(snapshot, operations) {
2404
2619
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2405
2620
  }
2406
2621
  }
2622
+ if (update.viewQuery !== void 0) {
2623
+ collection.viewQuery = update.viewQuery;
2624
+ }
2407
2625
  if (Object.keys(update.rulesToUpdate).length > 0) {
2408
2626
  if (!collection.rules) collection.rules = {};
2409
2627
  if (!collection.permissions) collection.permissions = {};
@@ -2867,7 +3085,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2867
3085
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2868
3086
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2869
3087
  }
3088
+ function normalizeSql(query) {
3089
+ if (!query) {
3090
+ return "";
3091
+ }
3092
+ return query.split("\n").map((line) => line.trim()).join(" ").replace(/\s+/g, " ").trim();
3093
+ }
3094
+ function compareViewQuery(currentCollection, previousCollection) {
3095
+ const newValue = currentCollection.viewQuery;
3096
+ if (typeof newValue !== "string") {
3097
+ return void 0;
3098
+ }
3099
+ if (normalizeSql(newValue) === normalizeSql(previousCollection.viewQuery)) {
3100
+ return void 0;
3101
+ }
3102
+ return {
3103
+ oldValue: previousCollection.viewQuery ?? null,
3104
+ newValue
3105
+ };
3106
+ }
2870
3107
  function buildCollectionModification(currentCollection, previousCollection, config, collectionIdToName) {
3108
+ const isView = currentCollection.type === "view" || previousCollection.type === "view";
3109
+ if (isView) {
3110
+ return {
3111
+ collection: currentCollection.name,
3112
+ fieldsToAdd: [],
3113
+ fieldsToRemove: [],
3114
+ fieldsToModify: [],
3115
+ indexesToAdd: [],
3116
+ indexesToRemove: [],
3117
+ rulesToUpdate: compareRules(
3118
+ currentCollection.rules,
3119
+ previousCollection.rules,
3120
+ currentCollection.permissions,
3121
+ previousCollection.permissions
3122
+ ),
3123
+ permissionsToUpdate: comparePermissions(currentCollection.permissions, previousCollection.permissions),
3124
+ viewQueryUpdate: compareViewQuery(currentCollection, previousCollection)
3125
+ };
3126
+ }
2871
3127
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2872
3128
  currentCollection,
2873
3129
  previousCollection,
@@ -2899,7 +3155,11 @@ function categorizeChangesBySeverity(diff, _config) {
2899
3155
  const destructive = [];
2900
3156
  const nonDestructive = [];
2901
3157
  for (const collection of diff.collectionsToDelete) {
2902
- destructive.push(`Delete collection: ${collection.name}`);
3158
+ if (collection.type === "view") {
3159
+ nonDestructive.push(`Delete view collection: ${collection.name}`);
3160
+ } else {
3161
+ destructive.push(`Delete collection: ${collection.name}`);
3162
+ }
2903
3163
  }
2904
3164
  for (const collection of diff.collectionsToCreate) {
2905
3165
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2934,6 +3194,9 @@ function categorizeChangesBySeverity(diff, _config) {
2934
3194
  for (const rule of modification.rulesToUpdate) {
2935
3195
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2936
3196
  }
3197
+ if (modification.viewQueryUpdate) {
3198
+ nonDestructive.push(`Update view query: ${collectionName}`);
3199
+ }
2937
3200
  }
2938
3201
  return { destructive, nonDestructive };
2939
3202
  }
@@ -2962,12 +3225,11 @@ function filterDiff(diff, options) {
2962
3225
  });
2963
3226
  let collectionsToDelete = diff.collectionsToDelete;
2964
3227
  if (skipDestructive) {
2965
- collectionsToDelete = [];
2966
- } else {
2967
- collectionsToDelete = collectionsToDelete.filter((col) => {
2968
- return matchesPattern(col.name, patterns);
2969
- });
3228
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
2970
3229
  }
3230
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3231
+ return matchesPattern(col.name, patterns);
3232
+ });
2971
3233
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
2972
3234
  const collectionMatches = matchesPattern(mod.collection, patterns);
2973
3235
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -2992,6 +3254,7 @@ function filterDiff(diff, options) {
2992
3254
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
2993
3255
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
2994
3256
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3257
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
2995
3258
  return {
2996
3259
  ...mod,
2997
3260
  fieldsToAdd,
@@ -3000,10 +3263,11 @@ function filterDiff(diff, options) {
3000
3263
  indexesToAdd,
3001
3264
  indexesToRemove,
3002
3265
  rulesToUpdate,
3003
- permissionsToUpdate
3266
+ permissionsToUpdate,
3267
+ viewQueryUpdate
3004
3268
  };
3005
3269
  }).filter((mod) => {
3006
- 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;
3270
+ 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;
3007
3271
  });
3008
3272
  return {
3009
3273
  ...diff,
@@ -3015,7 +3279,7 @@ function filterDiff(diff, options) {
3015
3279
 
3016
3280
  // src/migration/diff/index.ts
3017
3281
  function hasChanges(modification) {
3018
- 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;
3282
+ 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;
3019
3283
  }
3020
3284
  function aggregateChanges(currentSchema, previousSnapshot, config) {
3021
3285
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3194,6 +3458,13 @@ function formatValue(value) {
3194
3458
  }
3195
3459
  return String(value);
3196
3460
  }
3461
+ function formatSqlTemplate(query, indent = " ") {
3462
+ const escaped = query.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
3463
+ const body = escaped.split("\n").map((line) => line.trim() === "" ? "" : `${indent}${line}`).join("\n");
3464
+ return `\`
3465
+ ${body}
3466
+ ${indent.slice(0, -2)}\``;
3467
+ }
3197
3468
  function getFieldConstructorName(fieldType) {
3198
3469
  const constructorMap = {
3199
3470
  text: "TextField",
@@ -3232,6 +3503,36 @@ function getSystemFields() {
3232
3503
  }
3233
3504
  ];
3234
3505
  }
3506
+ function getSystemTimestampFields() {
3507
+ return [
3508
+ {
3509
+ name: "created",
3510
+ id: "autodate2990389176",
3511
+ type: "autodate",
3512
+ required: false,
3513
+ options: {
3514
+ hidden: false,
3515
+ onCreate: true,
3516
+ onUpdate: false,
3517
+ presentable: false,
3518
+ system: true
3519
+ }
3520
+ },
3521
+ {
3522
+ name: "updated",
3523
+ id: "autodate3332085495",
3524
+ type: "autodate",
3525
+ required: false,
3526
+ options: {
3527
+ hidden: false,
3528
+ onCreate: true,
3529
+ onUpdate: true,
3530
+ presentable: false,
3531
+ system: true
3532
+ }
3533
+ }
3534
+ ];
3535
+ }
3235
3536
  function getAuthSystemFields() {
3236
3537
  return [
3237
3538
  {
@@ -3524,6 +3825,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3524
3825
  if (!rules) {
3525
3826
  return "";
3526
3827
  }
3828
+ if (collectionType === "view") {
3829
+ return [
3830
+ `"listRule": ${formatValue(rules.listRule ?? null)}`,
3831
+ `"viewRule": ${formatValue(rules.viewRule ?? null)}`,
3832
+ `"createRule": null`,
3833
+ `"updateRule": null`,
3834
+ `"deleteRule": null`
3835
+ ].join(",\n ");
3836
+ }
3527
3837
  const parts = [];
3528
3838
  if (rules.listRule !== void 0) {
3529
3839
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3549,6 +3859,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3549
3859
  if (!permissions) {
3550
3860
  return "";
3551
3861
  }
3862
+ if (collectionType === "view") {
3863
+ return [
3864
+ `"listRule": ${formatValue(permissions.listRule ?? null)}`,
3865
+ `"viewRule": ${formatValue(permissions.viewRule ?? null)}`,
3866
+ `"createRule": null`,
3867
+ `"updateRule": null`,
3868
+ `"deleteRule": null`
3869
+ ].join(",\n ");
3870
+ }
3552
3871
  const parts = [];
3553
3872
  if (permissions.listRule !== void 0) {
3554
3873
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3586,6 +3905,28 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3586
3905
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3587
3906
  return lines.join("\n");
3588
3907
  }
3908
+ function generateViewQueryUpdate(collectionName, viewQuery, varName, isLast = false, collectionIdMap) {
3909
+ const lines = [];
3910
+ const collectionVar = varName || `collection_${collectionName}_viewQuery`;
3911
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3912
+ lines.push(` unmarshal({`);
3913
+ lines.push(` "viewQuery": ${formatSqlTemplate(viewQuery, " ")},`);
3914
+ lines.push(` }, ${collectionVar})`);
3915
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3916
+ return lines.join("\n");
3917
+ }
3918
+ function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3919
+ const collectionVar = `collection_${collectionName}_${varSuffix}`;
3920
+ const lines = [];
3921
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3922
+ lines.push(` unmarshal({`);
3923
+ for (const entry of entries) {
3924
+ lines.push(` "${entry.ruleType}": ${formatValue(entry.value)},`);
3925
+ }
3926
+ lines.push(` }, ${collectionVar})`);
3927
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3928
+ return lines.join("\n");
3929
+ }
3589
3930
 
3590
3931
  // src/migration/generator/collections.ts
3591
3932
  function generateCollectionCreation(collection, varName = "collection", isLast = false, collectionIdMap) {
@@ -3604,16 +3945,24 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3604
3945
  } else if (rulesCode) {
3605
3946
  lines.push(` ${rulesCode},`);
3606
3947
  }
3948
+ if (collection.type === "view") {
3949
+ lines.push(` "viewQuery": ${formatSqlTemplate(collection.viewQuery ?? "")},`);
3950
+ lines.push(` });`);
3951
+ lines.push(``);
3952
+ lines.push(isLast ? ` return app.save(${varName});` : ` app.save(${varName});`);
3953
+ return lines.join("\n");
3954
+ }
3607
3955
  const systemFieldNames = ["created", "updated", "id"];
3608
3956
  if (collection.type === "auth") {
3609
3957
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
3610
3958
  }
3611
3959
  const userFields = collection.fields.filter((f) => !systemFieldNames.includes(f.name));
3612
- const allFields = [...getSystemFields().filter((f) => f.name === "id")];
3960
+ const allFields = [...getSystemFields()];
3613
3961
  if (collection.type === "auth") {
3614
3962
  allFields.push(...getAuthSystemFields());
3615
3963
  }
3616
3964
  allFields.push(...userFields);
3965
+ allFields.push(...getSystemTimestampFields());
3617
3966
  lines.push(` "fields": ${generateFieldsArray(allFields, collectionIdMap)},`);
3618
3967
  let allIndexes = [...collection.indexes || []];
3619
3968
  if (collection.type === "auth") {
@@ -3643,7 +3992,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3643
3992
  const modification = operation.modifications;
3644
3993
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3645
3994
  let operationCount = 0;
3646
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
3995
+ 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);
3996
+ if (modification.viewQueryUpdate) {
3997
+ operationCount++;
3998
+ const isLast = operationCount === totalOperations;
3999
+ lines.push(
4000
+ generateViewQueryUpdate(
4001
+ collectionName,
4002
+ modification.viewQueryUpdate.newValue,
4003
+ void 0,
4004
+ isLast,
4005
+ collectionIdMap
4006
+ )
4007
+ );
4008
+ if (!isLast) lines.push("");
4009
+ }
3647
4010
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3648
4011
  const field = modification.fieldsToAdd[i];
3649
4012
  operationCount++;
@@ -3683,23 +4046,29 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3683
4046
  if (!isLast) lines.push("");
3684
4047
  }
3685
4048
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3686
- for (const permission of modification.permissionsToUpdate) {
3687
- operationCount++;
4049
+ operationCount++;
4050
+ const isLast = operationCount === totalOperations;
4051
+ if (modification.permissionsToUpdate.length >= 2) {
4052
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.newValue }));
4053
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4054
+ } else {
4055
+ const permission = modification.permissionsToUpdate[0];
3688
4056
  const varName = `collection_${collectionName}_perm_${permission.ruleType}`;
3689
- const isLast = operationCount === totalOperations;
3690
- lines.push(
3691
- generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap)
3692
- );
3693
- if (!isLast) lines.push("");
4057
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap));
3694
4058
  }
4059
+ if (!isLast) lines.push("");
3695
4060
  } else if (modification.rulesToUpdate.length > 0) {
3696
- for (const rule of modification.rulesToUpdate) {
3697
- operationCount++;
4061
+ operationCount++;
4062
+ const isLast = operationCount === totalOperations;
4063
+ if (modification.rulesToUpdate.length >= 2) {
4064
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.newValue }));
4065
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4066
+ } else {
4067
+ const rule = modification.rulesToUpdate[0];
3698
4068
  const varName = `collection_${collectionName}_rule_${rule.ruleType}`;
3699
- const isLast = operationCount === totalOperations;
3700
4069
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.newValue, varName, isLast, collectionIdMap));
3701
- if (!isLast) lines.push("");
3702
4070
  }
4071
+ if (!isLast) lines.push("");
3703
4072
  }
3704
4073
  } else if (operation.type === "delete") {
3705
4074
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection.name;
@@ -3738,25 +4107,45 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3738
4107
  const modification = operation.modifications;
3739
4108
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3740
4109
  let operationCount = 0;
3741
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
4110
+ 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);
4111
+ if (modification.viewQueryUpdate) {
4112
+ operationCount++;
4113
+ const isLast = operationCount === totalOperations;
4114
+ lines.push(
4115
+ generateViewQueryUpdate(
4116
+ collectionName,
4117
+ modification.viewQueryUpdate.oldValue ?? "",
4118
+ `collection_${collectionName}_revert_viewQuery`,
4119
+ isLast,
4120
+ collectionIdMap
4121
+ )
4122
+ );
4123
+ if (!isLast) lines.push("");
4124
+ }
3742
4125
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3743
- for (const permission of modification.permissionsToUpdate) {
3744
- operationCount++;
4126
+ operationCount++;
4127
+ const isLast = operationCount === totalOperations;
4128
+ if (modification.permissionsToUpdate.length >= 2) {
4129
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.oldValue }));
4130
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4131
+ } else {
4132
+ const permission = modification.permissionsToUpdate[0];
3745
4133
  const varName = `collection_${collectionName}_revert_perm_${permission.ruleType}`;
3746
- const isLast = operationCount === totalOperations;
3747
- lines.push(
3748
- generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap)
3749
- );
3750
- if (!isLast) lines.push("");
4134
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap));
3751
4135
  }
4136
+ if (!isLast) lines.push("");
3752
4137
  } else if (modification.rulesToUpdate.length > 0) {
3753
- for (const rule of modification.rulesToUpdate) {
3754
- operationCount++;
4138
+ operationCount++;
4139
+ const isLast = operationCount === totalOperations;
4140
+ if (modification.rulesToUpdate.length >= 2) {
4141
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.oldValue }));
4142
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4143
+ } else {
4144
+ const rule = modification.rulesToUpdate[0];
3755
4145
  const varName = `collection_${collectionName}_revert_rule_${rule.ruleType}`;
3756
- const isLast = operationCount === totalOperations;
3757
4146
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.oldValue, varName, isLast, collectionIdMap));
3758
- if (!isLast) lines.push("");
3759
4147
  }
4148
+ if (!isLast) lines.push("");
3760
4149
  }
3761
4150
  for (let i = 0; i < modification.indexesToRemove.length; i++) {
3762
4151
  operationCount++;
@@ -4005,6 +4394,9 @@ function generate(diff, config) {
4005
4394
  function detectCollectionDeletions(diff) {
4006
4395
  const changes = [];
4007
4396
  for (const collection of diff.collectionsToDelete) {
4397
+ if (collection.type === "view") {
4398
+ continue;
4399
+ }
4008
4400
  changes.push({
4009
4401
  type: "collection_deletion" /* COLLECTION_DELETION */,
4010
4402
  description: `Delete collection: ${collection.name}`,
@@ -4415,7 +4807,11 @@ function formatChangeSummary(diff) {
4415
4807
  lines.push(chalk__default.default.green.bold(`\u2713 ${totalCollectionsToCreate} collection(s) to create:`));
4416
4808
  for (const collection of diff.collectionsToCreate) {
4417
4809
  lines.push(chalk__default.default.green(` + ${collection.name} (${collection.type})`));
4418
- lines.push(chalk__default.default.gray(` ${collection.fields.length} field(s)`));
4810
+ if (collection.type === "view") {
4811
+ lines.push(chalk__default.default.gray(` fields derived from the view query`));
4812
+ } else {
4813
+ lines.push(chalk__default.default.gray(` ${collection.fields.length} field(s)`));
4814
+ }
4419
4815
  }
4420
4816
  lines.push("");
4421
4817
  }
@@ -4460,6 +4856,9 @@ function formatChangeSummary(diff) {
4460
4856
  if (modification.rulesToUpdate.length > 0) {
4461
4857
  lines.push(chalk__default.default.yellow(` ~ ${modification.rulesToUpdate.length} rule(s) to update`));
4462
4858
  }
4859
+ if (modification.viewQueryUpdate) {
4860
+ lines.push(chalk__default.default.yellow(` ~ view query to update`));
4861
+ }
4463
4862
  lines.push("");
4464
4863
  }
4465
4864
  }
@@ -4845,11 +5244,14 @@ var TypeGenerator = class {
4845
5244
  }
4846
5245
  generateCollectionType(collection) {
4847
5246
  const typeName = this.toPascalCase(collection.name);
5247
+ const isView = collection.type === "view";
4848
5248
  const lines = [];
4849
5249
  lines.push(`export interface ${typeName}Record {`);
4850
5250
  lines.push(` id: string;`);
4851
- lines.push(` created: string;`);
4852
- lines.push(` updated: string;`);
5251
+ if (!isView) {
5252
+ lines.push(` created: string;`);
5253
+ lines.push(` updated: string;`);
5254
+ }
4853
5255
  lines.push(` collectionId: string;`);
4854
5256
  lines.push(` collectionName: "${collection.name}";`);
4855
5257
  for (const field of collection.fields) {
@@ -4861,7 +5263,7 @@ var TypeGenerator = class {
4861
5263
  lines.push(`}`);
4862
5264
  lines.push(``);
4863
5265
  const expandFields = [];
4864
- for (const field of collection.fields) {
5266
+ for (const field of isView ? [] : collection.fields) {
4865
5267
  if (field.type === "relation" && field.relation) {
4866
5268
  const targetCollection = field.relation.collection;
4867
5269
  let targetType = "RecordModel";