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
@@ -1,6 +1,6 @@
1
1
  export { MigrationConfig, StatusOutput, VerbosityLevel, createProgressBar, createSpinner, formatChangeSummary, formatDuration, formatStatusJson, getMigrationsDirectory, getSchemaDirectory, getVerbosity, loadConfig, logBox, logDebug, logError, logInfo, logKeyValue, logList, logSection, logStep, logSuccess, logTable, logTimed, logTimestamp, logWarning, setVerbosity, withProgress } from './utils/index.cjs';
2
2
  import 'ora';
3
- import '../types-CWHV6ATd.cjs';
3
+ import '../types-CnzfX6JH.cjs';
4
4
  import '../fields-B96iGprI.cjs';
5
5
  import 'zod';
6
6
 
@@ -1,6 +1,6 @@
1
1
  export { MigrationConfig, StatusOutput, VerbosityLevel, createProgressBar, createSpinner, formatChangeSummary, formatDuration, formatStatusJson, getMigrationsDirectory, getSchemaDirectory, getVerbosity, loadConfig, logBox, logDebug, logError, logInfo, logKeyValue, logList, logSection, logStep, logSuccess, logTable, logTimed, logTimestamp, logWarning, setVerbosity, withProgress } from './utils/index.js';
2
2
  import 'ora';
3
- import '../types-C2nGWHLV.js';
3
+ import '../types-Do3jyFBm.js';
4
4
  import '../fields-B96iGprI.js';
5
5
  import 'zod';
6
6
 
package/dist/cli/index.js CHANGED
@@ -1193,6 +1193,60 @@ function unwrapZodType(zodType) {
1193
1193
  }
1194
1194
  return unwrapped;
1195
1195
  }
1196
+ function dedentSql(value) {
1197
+ const lines = value.replace(/\r\n/g, "\n").split("\n");
1198
+ while (lines.length > 0 && lines[0].trim() === "") {
1199
+ lines.shift();
1200
+ }
1201
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1202
+ lines.pop();
1203
+ }
1204
+ if (lines.length === 0) {
1205
+ return "";
1206
+ }
1207
+ let minIndent = Infinity;
1208
+ for (const line of lines) {
1209
+ if (line.trim() === "") continue;
1210
+ const indent = line.length - line.trimStart().length;
1211
+ if (indent < minIndent) {
1212
+ minIndent = indent;
1213
+ }
1214
+ }
1215
+ if (!Number.isFinite(minIndent) || minIndent === 0) {
1216
+ return lines.map((line) => line.trimEnd()).join("\n");
1217
+ }
1218
+ return lines.map((line) => line.slice(minIndent).trimEnd()).join("\n");
1219
+ }
1220
+ function stripLeadingComments(query) {
1221
+ let remaining = query.trim();
1222
+ while (remaining.length > 0) {
1223
+ if (remaining.startsWith("--")) {
1224
+ const newline = remaining.indexOf("\n");
1225
+ remaining = newline === -1 ? "" : remaining.slice(newline + 1).trim();
1226
+ continue;
1227
+ }
1228
+ if (remaining.startsWith("/*")) {
1229
+ const end = remaining.indexOf("*/");
1230
+ remaining = end === -1 ? "" : remaining.slice(end + 2).trim();
1231
+ continue;
1232
+ }
1233
+ break;
1234
+ }
1235
+ return remaining;
1236
+ }
1237
+ function validateViewQuery(collectionName, viewQuery) {
1238
+ if (typeof viewQuery !== "string" || viewQuery.trim() === "") {
1239
+ throw new Error(
1240
+ `View collection "${collectionName}" requires a non-empty viewQuery. Provide the SQL SELECT statement backing the view.`
1241
+ );
1242
+ }
1243
+ const body = stripLeadingComments(viewQuery);
1244
+ if (!/^(select|with)\b/i.test(body)) {
1245
+ throw new Error(
1246
+ `View collection "${collectionName}" has an invalid viewQuery: it must start with SELECT or WITH. Received: ${body.slice(0, 40)}${body.length > 40 ? "..." : ""}`
1247
+ );
1248
+ }
1249
+ }
1196
1250
  function getCollectionNameFromFile(filePath) {
1197
1251
  const filename = path8.basename(filePath).replace(/\.(ts|js)$/, "");
1198
1252
  return toCollectionName(filename);
@@ -1216,13 +1270,26 @@ function extractCollectionTypeFromSchema(zodSchema) {
1216
1270
  }
1217
1271
  try {
1218
1272
  const metadata = JSON.parse(zodSchema.description);
1219
- if (metadata.type === "base" || metadata.type === "auth") {
1273
+ if (metadata.type === "base" || metadata.type === "auth" || metadata.type === "view") {
1220
1274
  return metadata.type;
1221
1275
  }
1222
1276
  } catch {
1223
1277
  }
1224
1278
  return null;
1225
1279
  }
1280
+ function extractViewQueryFromSchema(zodSchema) {
1281
+ if (!zodSchema.description) {
1282
+ return null;
1283
+ }
1284
+ try {
1285
+ const metadata = JSON.parse(zodSchema.description);
1286
+ if (typeof metadata.viewQuery === "string") {
1287
+ return metadata.viewQuery;
1288
+ }
1289
+ } catch {
1290
+ }
1291
+ return null;
1292
+ }
1226
1293
  function extractSchemaDefinitions(module, patterns = ["Schema", "InputSchema", "Collection"]) {
1227
1294
  const result = {};
1228
1295
  if (module.default instanceof z.ZodObject) {
@@ -1381,6 +1448,15 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1381
1448
  const rawFields = extractFieldDefinitions(zodSchema);
1382
1449
  const explicitType = extractCollectionTypeFromSchema(zodSchema);
1383
1450
  const collectionType = explicitType ?? (isAuthCollection(rawFields) ? "auth" : "base");
1451
+ const isView = collectionType === "view";
1452
+ const viewQuery = extractViewQueryFromSchema(zodSchema);
1453
+ if (isView) {
1454
+ validateViewQuery(collectionName, viewQuery);
1455
+ } else if (viewQuery !== null) {
1456
+ console.warn(
1457
+ `[${collectionName}] viewQuery is only used by view collections and will be ignored. Use defineView() or set type: "view" to define a view collection.`
1458
+ );
1459
+ }
1384
1460
  const fields = rawFields.filter((f) => !["created", "updated"].includes(f.name)).map(({ name, zodType }) => buildFieldDefinition(name, zodType));
1385
1461
  if (collectionType === "auth") {
1386
1462
  const authSystemFields = [
@@ -1440,26 +1516,43 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1440
1516
  }
1441
1517
  }
1442
1518
  const indexes = extractIndexes(zodSchema) || [];
1519
+ if (isView && indexes.length > 0) {
1520
+ throw new Error(
1521
+ `View collection "${collectionName}" cannot declare indexes. PocketBase view collections are backed by a SQL query and do not support indexes.`
1522
+ );
1523
+ }
1443
1524
  const permissionAnalyzer = new PermissionAnalyzer();
1444
1525
  let permissions = void 0;
1445
1526
  const schemaDescription = zodSchema.description;
1446
1527
  const extractedPermissions = permissionAnalyzer.extractPermissions(schemaDescription);
1447
1528
  if (extractedPermissions) {
1448
1529
  const resolvedPermissions = permissionAnalyzer.resolvePermissions(extractedPermissions);
1449
- const validationResults = permissionAnalyzer.validatePermissions(
1450
- collectionName,
1451
- resolvedPermissions,
1452
- fields,
1453
- collectionType === "auth"
1454
- );
1455
- for (const [ruleType, result] of validationResults) {
1456
- if (!result.valid) {
1457
- console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1458
- result.errors.forEach((error) => console.error(` - ${error}`));
1530
+ if (isView) {
1531
+ for (const ruleType of ["createRule", "updateRule", "deleteRule", "manageRule"]) {
1532
+ if (resolvedPermissions[ruleType]) {
1533
+ console.warn(
1534
+ `[${collectionName}] ${ruleType} is not supported on view collections and will be set to null.`
1535
+ );
1536
+ }
1537
+ resolvedPermissions[ruleType] = null;
1459
1538
  }
1460
- if (result.warnings.length > 0) {
1461
- console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1462
- result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1539
+ }
1540
+ if (!isView) {
1541
+ const validationResults = permissionAnalyzer.validatePermissions(
1542
+ collectionName,
1543
+ resolvedPermissions,
1544
+ fields,
1545
+ collectionType === "auth"
1546
+ );
1547
+ for (const [ruleType, result] of validationResults) {
1548
+ if (!result.valid) {
1549
+ console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1550
+ result.errors.forEach((error) => console.error(` - ${error}`));
1551
+ }
1552
+ if (result.warnings.length > 0) {
1553
+ console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1554
+ result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1555
+ }
1463
1556
  }
1464
1557
  }
1465
1558
  permissions = permissionAnalyzer.mergeWithDefaults(resolvedPermissions);
@@ -1480,6 +1573,9 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1480
1573
  },
1481
1574
  permissions
1482
1575
  };
1576
+ if (isView) {
1577
+ collectionSchema.viewQuery = viewQuery;
1578
+ }
1483
1579
  return collectionSchema;
1484
1580
  }
1485
1581
  var tsxLoaderRegistered = false;
@@ -1737,8 +1833,9 @@ function convertPocketBaseField(pbField) {
1737
1833
  }
1738
1834
  function convertPocketBaseCollection(pbCollection) {
1739
1835
  const fields = [];
1836
+ const isView = pbCollection.type === "view";
1740
1837
  const systemFieldNames = ["id", "created", "updated", "collectionId", "collectionName", "expand"];
1741
- if (pbCollection.fields && Array.isArray(pbCollection.fields)) {
1838
+ if (!isView && pbCollection.fields && Array.isArray(pbCollection.fields)) {
1742
1839
  for (const pbField of pbCollection.fields) {
1743
1840
  if (pbField.system || systemFieldNames.includes(pbField.name)) {
1744
1841
  const isAuthSystemField = pbCollection.type === "auth" && ["email", "emailVisibility", "verified", "password", "tokenKey"].includes(pbField.name);
@@ -1758,6 +1855,9 @@ function convertPocketBaseCollection(pbCollection) {
1758
1855
  if (pbCollection.id) {
1759
1856
  schema.id = pbCollection.id;
1760
1857
  }
1858
+ if (typeof pbCollection.viewQuery === "string") {
1859
+ schema.viewQuery = dedentSql(pbCollection.viewQuery);
1860
+ }
1761
1861
  if (pbCollection.indexes && Array.isArray(pbCollection.indexes)) {
1762
1862
  schema.indexes = pbCollection.indexes;
1763
1863
  }
@@ -1859,6 +1959,24 @@ function findMigrationsAfterSnapshot(migrationsPath, snapshotTimestamp) {
1859
1959
  return [];
1860
1960
  }
1861
1961
  }
1962
+ function skipTemplateLiteral(content, start) {
1963
+ let i = start + 1;
1964
+ while (i < content.length) {
1965
+ const char = content[i];
1966
+ if (char === "\\") {
1967
+ i += 2;
1968
+ continue;
1969
+ }
1970
+ if (char === "`") {
1971
+ return i + 1;
1972
+ }
1973
+ i++;
1974
+ }
1975
+ return i;
1976
+ }
1977
+ function unescapeTemplateLiteral(raw) {
1978
+ return raw.replace(/\\(`|\$\{|\\)/g, "$1");
1979
+ }
1862
1980
  function parseMigrationOperationsFromContent(content) {
1863
1981
  const collectionsToCreate = [];
1864
1982
  const collectionsToDelete = [];
@@ -1947,6 +2065,10 @@ function parseMigrationOperationsFromContent(content) {
1947
2065
  while (i < content.length && bCount > 0) {
1948
2066
  const char = content[i];
1949
2067
  const prev = i > 0 ? content[i - 1] : "";
2068
+ if (!inStr && char === "`") {
2069
+ i = skipTemplateLiteral(content, i);
2070
+ continue;
2071
+ }
1950
2072
  if (!inStr && (char === '"' || char === "'")) {
1951
2073
  inStr = true;
1952
2074
  strChar = char;
@@ -2136,6 +2258,27 @@ function parseMigrationOperationsFromContent(content) {
2136
2258
  }
2137
2259
  }
2138
2260
  }
2261
+ const viewQueryRegex = /(\w+)\.viewQuery\s*=\s*/g;
2262
+ let viewQueryMatch;
2263
+ while ((viewQueryMatch = viewQueryRegex.exec(content)) !== null) {
2264
+ const varInfo = variables.get(viewQueryMatch[1]);
2265
+ if (!varInfo || varInfo.type !== "collection") continue;
2266
+ const valueStart = viewQueryMatch.index + viewQueryMatch[0].length;
2267
+ if (content[valueStart] === "`") {
2268
+ const end = skipTemplateLiteral(content, valueStart);
2269
+ const raw = content.substring(valueStart + 1, end - 1);
2270
+ getUpdate(varInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2271
+ viewQueryRegex.lastIndex = end;
2272
+ } else {
2273
+ const terminator = content.indexOf(";", valueStart);
2274
+ const valueStr = content.substring(valueStart, terminator === -1 ? content.length : terminator);
2275
+ try {
2276
+ getUpdate(varInfo.name).viewQuery = dedentSql(new Function("app", `return ${valueStr}`)(mockApp));
2277
+ } catch {
2278
+ getUpdate(varInfo.name).viewQuery = dedentSql(valueStr.trim());
2279
+ }
2280
+ }
2281
+ }
2139
2282
  const unmarshalRegex = /unmarshal\s*\(/g;
2140
2283
  let unmarshalMatch;
2141
2284
  while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
@@ -2150,6 +2293,10 @@ function parseMigrationOperationsFromContent(content) {
2150
2293
  while (i < content.length && braceCount > 0) {
2151
2294
  const ch = content[i];
2152
2295
  const prev = i > 0 ? content[i - 1] : "";
2296
+ if (!inStr && ch === "`") {
2297
+ i = skipTemplateLiteral(content, i);
2298
+ continue;
2299
+ }
2153
2300
  if (!inStr && (ch === '"' || ch === "'")) {
2154
2301
  inStr = true;
2155
2302
  strChar = ch;
@@ -2183,6 +2330,22 @@ function parseMigrationOperationsFromContent(content) {
2183
2330
  getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2184
2331
  }
2185
2332
  }
2333
+ const viewQueryKey = objStr.match(/"viewQuery"\s*:\s*/);
2334
+ if (viewQueryKey) {
2335
+ const valueStart = viewQueryKey.index + viewQueryKey[0].length;
2336
+ if (objStr[valueStart] === "`") {
2337
+ const end = skipTemplateLiteral(objStr, valueStart);
2338
+ const raw = objStr.substring(valueStart + 1, end - 1);
2339
+ getUpdate(colInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2340
+ } else {
2341
+ const valStr = objStr.substring(valueStart).replace(/[,}\s]+$/, "");
2342
+ try {
2343
+ getUpdate(colInfo.name).viewQuery = dedentSql(new Function("app", `return ${valStr}`)(mockApp));
2344
+ } catch {
2345
+ getUpdate(colInfo.name).viewQuery = dedentSql(valStr);
2346
+ }
2347
+ }
2348
+ }
2186
2349
  }
2187
2350
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2188
2351
  let match;
@@ -2313,6 +2476,10 @@ function parseMigrationOperations(migrationContent) {
2313
2476
  while (i < migrationContent.length && braceCount > 0) {
2314
2477
  const char = migrationContent[i];
2315
2478
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2479
+ if (!inString && char === "`") {
2480
+ i = skipTemplateLiteral(migrationContent, i);
2481
+ continue;
2482
+ }
2316
2483
  if (!inString && (char === '"' || char === "'")) {
2317
2484
  inString = true;
2318
2485
  stringChar = char;
@@ -2420,6 +2587,9 @@ function applyMigrationOperations(snapshot, operations) {
2420
2587
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2421
2588
  }
2422
2589
  }
2590
+ if (update.viewQuery !== void 0) {
2591
+ collection.viewQuery = update.viewQuery;
2592
+ }
2423
2593
  if (Object.keys(update.rulesToUpdate).length > 0) {
2424
2594
  if (!collection.rules) collection.rules = {};
2425
2595
  if (!collection.permissions) collection.permissions = {};
@@ -2883,7 +3053,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2883
3053
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2884
3054
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2885
3055
  }
3056
+ function normalizeSql(query) {
3057
+ if (!query) {
3058
+ return "";
3059
+ }
3060
+ return query.split("\n").map((line) => line.trim()).join(" ").replace(/\s+/g, " ").trim();
3061
+ }
3062
+ function compareViewQuery(currentCollection, previousCollection) {
3063
+ const newValue = currentCollection.viewQuery;
3064
+ if (typeof newValue !== "string") {
3065
+ return void 0;
3066
+ }
3067
+ if (normalizeSql(newValue) === normalizeSql(previousCollection.viewQuery)) {
3068
+ return void 0;
3069
+ }
3070
+ return {
3071
+ oldValue: previousCollection.viewQuery ?? null,
3072
+ newValue
3073
+ };
3074
+ }
2886
3075
  function buildCollectionModification(currentCollection, previousCollection, config, collectionIdToName) {
3076
+ const isView = currentCollection.type === "view" || previousCollection.type === "view";
3077
+ if (isView) {
3078
+ return {
3079
+ collection: currentCollection.name,
3080
+ fieldsToAdd: [],
3081
+ fieldsToRemove: [],
3082
+ fieldsToModify: [],
3083
+ indexesToAdd: [],
3084
+ indexesToRemove: [],
3085
+ rulesToUpdate: compareRules(
3086
+ currentCollection.rules,
3087
+ previousCollection.rules,
3088
+ currentCollection.permissions,
3089
+ previousCollection.permissions
3090
+ ),
3091
+ permissionsToUpdate: comparePermissions(currentCollection.permissions, previousCollection.permissions),
3092
+ viewQueryUpdate: compareViewQuery(currentCollection, previousCollection)
3093
+ };
3094
+ }
2887
3095
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2888
3096
  currentCollection,
2889
3097
  previousCollection,
@@ -2915,7 +3123,11 @@ function categorizeChangesBySeverity(diff, _config) {
2915
3123
  const destructive = [];
2916
3124
  const nonDestructive = [];
2917
3125
  for (const collection of diff.collectionsToDelete) {
2918
- destructive.push(`Delete collection: ${collection.name}`);
3126
+ if (collection.type === "view") {
3127
+ nonDestructive.push(`Delete view collection: ${collection.name}`);
3128
+ } else {
3129
+ destructive.push(`Delete collection: ${collection.name}`);
3130
+ }
2919
3131
  }
2920
3132
  for (const collection of diff.collectionsToCreate) {
2921
3133
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2950,6 +3162,9 @@ function categorizeChangesBySeverity(diff, _config) {
2950
3162
  for (const rule of modification.rulesToUpdate) {
2951
3163
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2952
3164
  }
3165
+ if (modification.viewQueryUpdate) {
3166
+ nonDestructive.push(`Update view query: ${collectionName}`);
3167
+ }
2953
3168
  }
2954
3169
  return { destructive, nonDestructive };
2955
3170
  }
@@ -2978,12 +3193,11 @@ function filterDiff(diff, options) {
2978
3193
  });
2979
3194
  let collectionsToDelete = diff.collectionsToDelete;
2980
3195
  if (skipDestructive) {
2981
- collectionsToDelete = [];
2982
- } else {
2983
- collectionsToDelete = collectionsToDelete.filter((col) => {
2984
- return matchesPattern(col.name, patterns);
2985
- });
3196
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
2986
3197
  }
3198
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3199
+ return matchesPattern(col.name, patterns);
3200
+ });
2987
3201
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
2988
3202
  const collectionMatches = matchesPattern(mod.collection, patterns);
2989
3203
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -3008,6 +3222,7 @@ function filterDiff(diff, options) {
3008
3222
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
3009
3223
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
3010
3224
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3225
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
3011
3226
  return {
3012
3227
  ...mod,
3013
3228
  fieldsToAdd,
@@ -3016,10 +3231,11 @@ function filterDiff(diff, options) {
3016
3231
  indexesToAdd,
3017
3232
  indexesToRemove,
3018
3233
  rulesToUpdate,
3019
- permissionsToUpdate
3234
+ permissionsToUpdate,
3235
+ viewQueryUpdate
3020
3236
  };
3021
3237
  }).filter((mod) => {
3022
- return mod.fieldsToAdd.length > 0 || mod.fieldsToRemove.length > 0 || mod.fieldsToModify.length > 0 || mod.indexesToAdd.length > 0 || mod.indexesToRemove.length > 0 || mod.rulesToUpdate.length > 0 || mod.permissionsToUpdate.length > 0;
3238
+ return mod.fieldsToAdd.length > 0 || mod.fieldsToRemove.length > 0 || mod.fieldsToModify.length > 0 || mod.indexesToAdd.length > 0 || mod.indexesToRemove.length > 0 || mod.rulesToUpdate.length > 0 || mod.permissionsToUpdate.length > 0 || mod.viewQueryUpdate !== void 0;
3023
3239
  });
3024
3240
  return {
3025
3241
  ...diff,
@@ -3031,7 +3247,7 @@ function filterDiff(diff, options) {
3031
3247
 
3032
3248
  // src/migration/diff/index.ts
3033
3249
  function hasChanges(modification) {
3034
- return modification.fieldsToAdd.length > 0 || modification.fieldsToRemove.length > 0 || modification.fieldsToModify.length > 0 || modification.indexesToAdd.length > 0 || modification.indexesToRemove.length > 0 || modification.rulesToUpdate.length > 0 || modification.permissionsToUpdate.length > 0;
3250
+ return modification.fieldsToAdd.length > 0 || modification.fieldsToRemove.length > 0 || modification.fieldsToModify.length > 0 || modification.indexesToAdd.length > 0 || modification.indexesToRemove.length > 0 || modification.rulesToUpdate.length > 0 || modification.permissionsToUpdate.length > 0 || modification.viewQueryUpdate !== void 0;
3035
3251
  }
3036
3252
  function aggregateChanges(currentSchema, previousSnapshot, config) {
3037
3253
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3210,6 +3426,13 @@ function formatValue(value) {
3210
3426
  }
3211
3427
  return String(value);
3212
3428
  }
3429
+ function formatSqlTemplate(query, indent = " ") {
3430
+ const escaped = query.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
3431
+ const body = escaped.split("\n").map((line) => line.trim() === "" ? "" : `${indent}${line}`).join("\n");
3432
+ return `\`
3433
+ ${body}
3434
+ ${indent.slice(0, -2)}\``;
3435
+ }
3213
3436
  function getFieldConstructorName(fieldType) {
3214
3437
  const constructorMap = {
3215
3438
  text: "TextField",
@@ -3570,6 +3793,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3570
3793
  if (!rules) {
3571
3794
  return "";
3572
3795
  }
3796
+ if (collectionType === "view") {
3797
+ return [
3798
+ `"listRule": ${formatValue(rules.listRule ?? null)}`,
3799
+ `"viewRule": ${formatValue(rules.viewRule ?? null)}`,
3800
+ `"createRule": null`,
3801
+ `"updateRule": null`,
3802
+ `"deleteRule": null`
3803
+ ].join(",\n ");
3804
+ }
3573
3805
  const parts = [];
3574
3806
  if (rules.listRule !== void 0) {
3575
3807
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3595,6 +3827,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3595
3827
  if (!permissions) {
3596
3828
  return "";
3597
3829
  }
3830
+ if (collectionType === "view") {
3831
+ return [
3832
+ `"listRule": ${formatValue(permissions.listRule ?? null)}`,
3833
+ `"viewRule": ${formatValue(permissions.viewRule ?? null)}`,
3834
+ `"createRule": null`,
3835
+ `"updateRule": null`,
3836
+ `"deleteRule": null`
3837
+ ].join(",\n ");
3838
+ }
3598
3839
  const parts = [];
3599
3840
  if (permissions.listRule !== void 0) {
3600
3841
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3632,6 +3873,16 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3632
3873
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3633
3874
  return lines.join("\n");
3634
3875
  }
3876
+ function generateViewQueryUpdate(collectionName, viewQuery, varName, isLast = false, collectionIdMap) {
3877
+ const lines = [];
3878
+ const collectionVar = varName || `collection_${collectionName}_viewQuery`;
3879
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3880
+ lines.push(` unmarshal({`);
3881
+ lines.push(` "viewQuery": ${formatSqlTemplate(viewQuery, " ")},`);
3882
+ lines.push(` }, ${collectionVar})`);
3883
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3884
+ return lines.join("\n");
3885
+ }
3635
3886
  function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3636
3887
  const collectionVar = `collection_${collectionName}_${varSuffix}`;
3637
3888
  const lines = [];
@@ -3662,6 +3913,13 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3662
3913
  } else if (rulesCode) {
3663
3914
  lines.push(` ${rulesCode},`);
3664
3915
  }
3916
+ if (collection.type === "view") {
3917
+ lines.push(` "viewQuery": ${formatSqlTemplate(collection.viewQuery ?? "")},`);
3918
+ lines.push(` });`);
3919
+ lines.push(``);
3920
+ lines.push(isLast ? ` return app.save(${varName});` : ` app.save(${varName});`);
3921
+ return lines.join("\n");
3922
+ }
3665
3923
  const systemFieldNames = ["created", "updated", "id"];
3666
3924
  if (collection.type === "auth") {
3667
3925
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
@@ -3702,7 +3960,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3702
3960
  const modification = operation.modifications;
3703
3961
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3704
3962
  let operationCount = 0;
3705
- 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);
3963
+ const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + (modification.viewQueryUpdate ? 1 : 0) + (modification.permissionsToUpdate.length > 0 ? 1 : modification.rulesToUpdate.length > 0 ? 1 : 0);
3964
+ if (modification.viewQueryUpdate) {
3965
+ operationCount++;
3966
+ const isLast = operationCount === totalOperations;
3967
+ lines.push(
3968
+ generateViewQueryUpdate(
3969
+ collectionName,
3970
+ modification.viewQueryUpdate.newValue,
3971
+ void 0,
3972
+ isLast,
3973
+ collectionIdMap
3974
+ )
3975
+ );
3976
+ if (!isLast) lines.push("");
3977
+ }
3706
3978
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3707
3979
  const field = modification.fieldsToAdd[i];
3708
3980
  operationCount++;
@@ -3803,7 +4075,21 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3803
4075
  const modification = operation.modifications;
3804
4076
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3805
4077
  let operationCount = 0;
3806
- 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);
4078
+ const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + (modification.viewQueryUpdate ? 1 : 0) + (modification.permissionsToUpdate.length > 0 ? 1 : modification.rulesToUpdate.length > 0 ? 1 : 0);
4079
+ if (modification.viewQueryUpdate) {
4080
+ operationCount++;
4081
+ const isLast = operationCount === totalOperations;
4082
+ lines.push(
4083
+ generateViewQueryUpdate(
4084
+ collectionName,
4085
+ modification.viewQueryUpdate.oldValue ?? "",
4086
+ `collection_${collectionName}_revert_viewQuery`,
4087
+ isLast,
4088
+ collectionIdMap
4089
+ )
4090
+ );
4091
+ if (!isLast) lines.push("");
4092
+ }
3807
4093
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3808
4094
  operationCount++;
3809
4095
  const isLast = operationCount === totalOperations;
@@ -4076,6 +4362,9 @@ function generate(diff, config) {
4076
4362
  function detectCollectionDeletions(diff) {
4077
4363
  const changes = [];
4078
4364
  for (const collection of diff.collectionsToDelete) {
4365
+ if (collection.type === "view") {
4366
+ continue;
4367
+ }
4079
4368
  changes.push({
4080
4369
  type: "collection_deletion" /* COLLECTION_DELETION */,
4081
4370
  description: `Delete collection: ${collection.name}`,
@@ -4489,7 +4778,11 @@ function formatChangeSummary(diff) {
4489
4778
  lines.push(chalk.green.bold(`\u2713 ${totalCollectionsToCreate} collection(s) to create:`));
4490
4779
  for (const collection of diff.collectionsToCreate) {
4491
4780
  lines.push(chalk.green(` + ${collection.name} (${collection.type})`));
4492
- lines.push(chalk.gray(` ${collection.fields.length} field(s)`));
4781
+ if (collection.type === "view") {
4782
+ lines.push(chalk.gray(` fields derived from the view query`));
4783
+ } else {
4784
+ lines.push(chalk.gray(` ${collection.fields.length} field(s)`));
4785
+ }
4493
4786
  }
4494
4787
  lines.push("");
4495
4788
  }
@@ -4534,6 +4827,9 @@ function formatChangeSummary(diff) {
4534
4827
  if (modification.rulesToUpdate.length > 0) {
4535
4828
  lines.push(chalk.yellow(` ~ ${modification.rulesToUpdate.length} rule(s) to update`));
4536
4829
  }
4830
+ if (modification.viewQueryUpdate) {
4831
+ lines.push(chalk.yellow(` ~ view query to update`));
4832
+ }
4537
4833
  lines.push("");
4538
4834
  }
4539
4835
  }