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
@@ -1196,6 +1196,60 @@ function unwrapZodType(zodType) {
1196
1196
  }
1197
1197
  return unwrapped;
1198
1198
  }
1199
+ function dedentSql(value) {
1200
+ const lines = value.replace(/\r\n/g, "\n").split("\n");
1201
+ while (lines.length > 0 && lines[0].trim() === "") {
1202
+ lines.shift();
1203
+ }
1204
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1205
+ lines.pop();
1206
+ }
1207
+ if (lines.length === 0) {
1208
+ return "";
1209
+ }
1210
+ let minIndent = Infinity;
1211
+ for (const line of lines) {
1212
+ if (line.trim() === "") continue;
1213
+ const indent = line.length - line.trimStart().length;
1214
+ if (indent < minIndent) {
1215
+ minIndent = indent;
1216
+ }
1217
+ }
1218
+ if (!Number.isFinite(minIndent) || minIndent === 0) {
1219
+ return lines.map((line) => line.trimEnd()).join("\n");
1220
+ }
1221
+ return lines.map((line) => line.slice(minIndent).trimEnd()).join("\n");
1222
+ }
1223
+ function stripLeadingComments(query) {
1224
+ let remaining = query.trim();
1225
+ while (remaining.length > 0) {
1226
+ if (remaining.startsWith("--")) {
1227
+ const newline = remaining.indexOf("\n");
1228
+ remaining = newline === -1 ? "" : remaining.slice(newline + 1).trim();
1229
+ continue;
1230
+ }
1231
+ if (remaining.startsWith("/*")) {
1232
+ const end = remaining.indexOf("*/");
1233
+ remaining = end === -1 ? "" : remaining.slice(end + 2).trim();
1234
+ continue;
1235
+ }
1236
+ break;
1237
+ }
1238
+ return remaining;
1239
+ }
1240
+ function validateViewQuery(collectionName, viewQuery) {
1241
+ if (typeof viewQuery !== "string" || viewQuery.trim() === "") {
1242
+ throw new Error(
1243
+ `View collection "${collectionName}" requires a non-empty viewQuery. Provide the SQL SELECT statement backing the view.`
1244
+ );
1245
+ }
1246
+ const body = stripLeadingComments(viewQuery);
1247
+ if (!/^(select|with)\b/i.test(body)) {
1248
+ throw new Error(
1249
+ `View collection "${collectionName}" has an invalid viewQuery: it must start with SELECT or WITH. Received: ${body.slice(0, 40)}${body.length > 40 ? "..." : ""}`
1250
+ );
1251
+ }
1252
+ }
1199
1253
  function getCollectionNameFromFile(filePath) {
1200
1254
  const filename = path8.basename(filePath).replace(/\.(ts|js)$/, "");
1201
1255
  return toCollectionName(filename);
@@ -1219,13 +1273,26 @@ function extractCollectionTypeFromSchema(zodSchema) {
1219
1273
  }
1220
1274
  try {
1221
1275
  const metadata = JSON.parse(zodSchema.description);
1222
- if (metadata.type === "base" || metadata.type === "auth") {
1276
+ if (metadata.type === "base" || metadata.type === "auth" || metadata.type === "view") {
1223
1277
  return metadata.type;
1224
1278
  }
1225
1279
  } catch {
1226
1280
  }
1227
1281
  return null;
1228
1282
  }
1283
+ function extractViewQueryFromSchema(zodSchema) {
1284
+ if (!zodSchema.description) {
1285
+ return null;
1286
+ }
1287
+ try {
1288
+ const metadata = JSON.parse(zodSchema.description);
1289
+ if (typeof metadata.viewQuery === "string") {
1290
+ return metadata.viewQuery;
1291
+ }
1292
+ } catch {
1293
+ }
1294
+ return null;
1295
+ }
1229
1296
  function extractSchemaDefinitions(module, patterns = ["Schema", "InputSchema", "Collection"]) {
1230
1297
  const result = {};
1231
1298
  if (module.default instanceof z.ZodObject) {
@@ -1384,6 +1451,15 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1384
1451
  const rawFields = extractFieldDefinitions(zodSchema);
1385
1452
  const explicitType = extractCollectionTypeFromSchema(zodSchema);
1386
1453
  const collectionType = explicitType ?? (isAuthCollection(rawFields) ? "auth" : "base");
1454
+ const isView = collectionType === "view";
1455
+ const viewQuery = extractViewQueryFromSchema(zodSchema);
1456
+ if (isView) {
1457
+ validateViewQuery(collectionName, viewQuery);
1458
+ } else if (viewQuery !== null) {
1459
+ console.warn(
1460
+ `[${collectionName}] viewQuery is only used by view collections and will be ignored. Use defineView() or set type: "view" to define a view collection.`
1461
+ );
1462
+ }
1387
1463
  const fields = rawFields.filter((f) => !["created", "updated"].includes(f.name)).map(({ name, zodType }) => buildFieldDefinition(name, zodType));
1388
1464
  if (collectionType === "auth") {
1389
1465
  const authSystemFields = [
@@ -1443,26 +1519,43 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1443
1519
  }
1444
1520
  }
1445
1521
  const indexes = extractIndexes(zodSchema) || [];
1522
+ if (isView && indexes.length > 0) {
1523
+ throw new Error(
1524
+ `View collection "${collectionName}" cannot declare indexes. PocketBase view collections are backed by a SQL query and do not support indexes.`
1525
+ );
1526
+ }
1446
1527
  const permissionAnalyzer = new PermissionAnalyzer();
1447
1528
  let permissions = void 0;
1448
1529
  const schemaDescription = zodSchema.description;
1449
1530
  const extractedPermissions = permissionAnalyzer.extractPermissions(schemaDescription);
1450
1531
  if (extractedPermissions) {
1451
1532
  const resolvedPermissions = permissionAnalyzer.resolvePermissions(extractedPermissions);
1452
- const validationResults = permissionAnalyzer.validatePermissions(
1453
- collectionName,
1454
- resolvedPermissions,
1455
- fields,
1456
- collectionType === "auth"
1457
- );
1458
- for (const [ruleType, result] of validationResults) {
1459
- if (!result.valid) {
1460
- console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1461
- result.errors.forEach((error) => console.error(` - ${error}`));
1533
+ if (isView) {
1534
+ for (const ruleType of ["createRule", "updateRule", "deleteRule", "manageRule"]) {
1535
+ if (resolvedPermissions[ruleType]) {
1536
+ console.warn(
1537
+ `[${collectionName}] ${ruleType} is not supported on view collections and will be set to null.`
1538
+ );
1539
+ }
1540
+ resolvedPermissions[ruleType] = null;
1462
1541
  }
1463
- if (result.warnings.length > 0) {
1464
- console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1465
- result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1542
+ }
1543
+ if (!isView) {
1544
+ const validationResults = permissionAnalyzer.validatePermissions(
1545
+ collectionName,
1546
+ resolvedPermissions,
1547
+ fields,
1548
+ collectionType === "auth"
1549
+ );
1550
+ for (const [ruleType, result] of validationResults) {
1551
+ if (!result.valid) {
1552
+ console.error(`[${collectionName}] Permission validation failed for ${ruleType}:`);
1553
+ result.errors.forEach((error) => console.error(` - ${error}`));
1554
+ }
1555
+ if (result.warnings.length > 0) {
1556
+ console.warn(`[${collectionName}] Permission warnings for ${ruleType}:`);
1557
+ result.warnings.forEach((warning) => console.warn(` - ${warning}`));
1558
+ }
1466
1559
  }
1467
1560
  }
1468
1561
  permissions = permissionAnalyzer.mergeWithDefaults(resolvedPermissions);
@@ -1483,6 +1576,9 @@ function convertZodSchemaToCollectionSchema(collectionName, zodSchema) {
1483
1576
  },
1484
1577
  permissions
1485
1578
  };
1579
+ if (isView) {
1580
+ collectionSchema.viewQuery = viewQuery;
1581
+ }
1486
1582
  return collectionSchema;
1487
1583
  }
1488
1584
  var tsxLoaderRegistered = false;
@@ -1740,8 +1836,9 @@ function convertPocketBaseField(pbField) {
1740
1836
  }
1741
1837
  function convertPocketBaseCollection(pbCollection) {
1742
1838
  const fields = [];
1839
+ const isView = pbCollection.type === "view";
1743
1840
  const systemFieldNames = ["id", "created", "updated", "collectionId", "collectionName", "expand"];
1744
- if (pbCollection.fields && Array.isArray(pbCollection.fields)) {
1841
+ if (!isView && pbCollection.fields && Array.isArray(pbCollection.fields)) {
1745
1842
  for (const pbField of pbCollection.fields) {
1746
1843
  if (pbField.system || systemFieldNames.includes(pbField.name)) {
1747
1844
  const isAuthSystemField = pbCollection.type === "auth" && ["email", "emailVisibility", "verified", "password", "tokenKey"].includes(pbField.name);
@@ -1761,6 +1858,9 @@ function convertPocketBaseCollection(pbCollection) {
1761
1858
  if (pbCollection.id) {
1762
1859
  schema.id = pbCollection.id;
1763
1860
  }
1861
+ if (typeof pbCollection.viewQuery === "string") {
1862
+ schema.viewQuery = dedentSql(pbCollection.viewQuery);
1863
+ }
1764
1864
  if (pbCollection.indexes && Array.isArray(pbCollection.indexes)) {
1765
1865
  schema.indexes = pbCollection.indexes;
1766
1866
  }
@@ -1862,6 +1962,24 @@ function findMigrationsAfterSnapshot(migrationsPath, snapshotTimestamp) {
1862
1962
  return [];
1863
1963
  }
1864
1964
  }
1965
+ function skipTemplateLiteral(content, start) {
1966
+ let i = start + 1;
1967
+ while (i < content.length) {
1968
+ const char = content[i];
1969
+ if (char === "\\") {
1970
+ i += 2;
1971
+ continue;
1972
+ }
1973
+ if (char === "`") {
1974
+ return i + 1;
1975
+ }
1976
+ i++;
1977
+ }
1978
+ return i;
1979
+ }
1980
+ function unescapeTemplateLiteral(raw) {
1981
+ return raw.replace(/\\(`|\$\{|\\)/g, "$1");
1982
+ }
1865
1983
  function parseMigrationOperationsFromContent(content) {
1866
1984
  const collectionsToCreate = [];
1867
1985
  const collectionsToDelete = [];
@@ -1950,6 +2068,10 @@ function parseMigrationOperationsFromContent(content) {
1950
2068
  while (i < content.length && bCount > 0) {
1951
2069
  const char = content[i];
1952
2070
  const prev = i > 0 ? content[i - 1] : "";
2071
+ if (!inStr && char === "`") {
2072
+ i = skipTemplateLiteral(content, i);
2073
+ continue;
2074
+ }
1953
2075
  if (!inStr && (char === '"' || char === "'")) {
1954
2076
  inStr = true;
1955
2077
  strChar = char;
@@ -2139,6 +2261,27 @@ function parseMigrationOperationsFromContent(content) {
2139
2261
  }
2140
2262
  }
2141
2263
  }
2264
+ const viewQueryRegex = /(\w+)\.viewQuery\s*=\s*/g;
2265
+ let viewQueryMatch;
2266
+ while ((viewQueryMatch = viewQueryRegex.exec(content)) !== null) {
2267
+ const varInfo = variables.get(viewQueryMatch[1]);
2268
+ if (!varInfo || varInfo.type !== "collection") continue;
2269
+ const valueStart = viewQueryMatch.index + viewQueryMatch[0].length;
2270
+ if (content[valueStart] === "`") {
2271
+ const end = skipTemplateLiteral(content, valueStart);
2272
+ const raw = content.substring(valueStart + 1, end - 1);
2273
+ getUpdate(varInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2274
+ viewQueryRegex.lastIndex = end;
2275
+ } else {
2276
+ const terminator = content.indexOf(";", valueStart);
2277
+ const valueStr = content.substring(valueStart, terminator === -1 ? content.length : terminator);
2278
+ try {
2279
+ getUpdate(varInfo.name).viewQuery = dedentSql(new Function("app", `return ${valueStr}`)(mockApp));
2280
+ } catch {
2281
+ getUpdate(varInfo.name).viewQuery = dedentSql(valueStr.trim());
2282
+ }
2283
+ }
2284
+ }
2142
2285
  const unmarshalRegex = /unmarshal\s*\(/g;
2143
2286
  let unmarshalMatch;
2144
2287
  while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
@@ -2153,6 +2296,10 @@ function parseMigrationOperationsFromContent(content) {
2153
2296
  while (i < content.length && braceCount > 0) {
2154
2297
  const ch = content[i];
2155
2298
  const prev = i > 0 ? content[i - 1] : "";
2299
+ if (!inStr && ch === "`") {
2300
+ i = skipTemplateLiteral(content, i);
2301
+ continue;
2302
+ }
2156
2303
  if (!inStr && (ch === '"' || ch === "'")) {
2157
2304
  inStr = true;
2158
2305
  strChar = ch;
@@ -2186,6 +2333,22 @@ function parseMigrationOperationsFromContent(content) {
2186
2333
  getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2187
2334
  }
2188
2335
  }
2336
+ const viewQueryKey = objStr.match(/"viewQuery"\s*:\s*/);
2337
+ if (viewQueryKey) {
2338
+ const valueStart = viewQueryKey.index + viewQueryKey[0].length;
2339
+ if (objStr[valueStart] === "`") {
2340
+ const end = skipTemplateLiteral(objStr, valueStart);
2341
+ const raw = objStr.substring(valueStart + 1, end - 1);
2342
+ getUpdate(colInfo.name).viewQuery = dedentSql(unescapeTemplateLiteral(raw));
2343
+ } else {
2344
+ const valStr = objStr.substring(valueStart).replace(/[,}\s]+$/, "");
2345
+ try {
2346
+ getUpdate(colInfo.name).viewQuery = dedentSql(new Function("app", `return ${valStr}`)(mockApp));
2347
+ } catch {
2348
+ getUpdate(colInfo.name).viewQuery = dedentSql(valStr);
2349
+ }
2350
+ }
2351
+ }
2189
2352
  }
2190
2353
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2191
2354
  let match;
@@ -2316,6 +2479,10 @@ function parseMigrationOperations(migrationContent) {
2316
2479
  while (i < migrationContent.length && braceCount > 0) {
2317
2480
  const char = migrationContent[i];
2318
2481
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2482
+ if (!inString && char === "`") {
2483
+ i = skipTemplateLiteral(migrationContent, i);
2484
+ continue;
2485
+ }
2319
2486
  if (!inString && (char === '"' || char === "'")) {
2320
2487
  inString = true;
2321
2488
  stringChar = char;
@@ -2423,6 +2590,9 @@ function applyMigrationOperations(snapshot, operations) {
2423
2590
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2424
2591
  }
2425
2592
  }
2593
+ if (update.viewQuery !== void 0) {
2594
+ collection.viewQuery = update.viewQuery;
2595
+ }
2426
2596
  if (Object.keys(update.rulesToUpdate).length > 0) {
2427
2597
  if (!collection.rules) collection.rules = {};
2428
2598
  if (!collection.permissions) collection.permissions = {};
@@ -2886,7 +3056,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2886
3056
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2887
3057
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2888
3058
  }
3059
+ function normalizeSql(query) {
3060
+ if (!query) {
3061
+ return "";
3062
+ }
3063
+ return query.split("\n").map((line) => line.trim()).join(" ").replace(/\s+/g, " ").trim();
3064
+ }
3065
+ function compareViewQuery(currentCollection, previousCollection) {
3066
+ const newValue = currentCollection.viewQuery;
3067
+ if (typeof newValue !== "string") {
3068
+ return void 0;
3069
+ }
3070
+ if (normalizeSql(newValue) === normalizeSql(previousCollection.viewQuery)) {
3071
+ return void 0;
3072
+ }
3073
+ return {
3074
+ oldValue: previousCollection.viewQuery ?? null,
3075
+ newValue
3076
+ };
3077
+ }
2889
3078
  function buildCollectionModification(currentCollection, previousCollection, config, collectionIdToName) {
3079
+ const isView = currentCollection.type === "view" || previousCollection.type === "view";
3080
+ if (isView) {
3081
+ return {
3082
+ collection: currentCollection.name,
3083
+ fieldsToAdd: [],
3084
+ fieldsToRemove: [],
3085
+ fieldsToModify: [],
3086
+ indexesToAdd: [],
3087
+ indexesToRemove: [],
3088
+ rulesToUpdate: compareRules(
3089
+ currentCollection.rules,
3090
+ previousCollection.rules,
3091
+ currentCollection.permissions,
3092
+ previousCollection.permissions
3093
+ ),
3094
+ permissionsToUpdate: comparePermissions(currentCollection.permissions, previousCollection.permissions),
3095
+ viewQueryUpdate: compareViewQuery(currentCollection, previousCollection)
3096
+ };
3097
+ }
2890
3098
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2891
3099
  currentCollection,
2892
3100
  previousCollection,
@@ -2918,7 +3126,11 @@ function categorizeChangesBySeverity(diff, _config) {
2918
3126
  const destructive = [];
2919
3127
  const nonDestructive = [];
2920
3128
  for (const collection of diff.collectionsToDelete) {
2921
- destructive.push(`Delete collection: ${collection.name}`);
3129
+ if (collection.type === "view") {
3130
+ nonDestructive.push(`Delete view collection: ${collection.name}`);
3131
+ } else {
3132
+ destructive.push(`Delete collection: ${collection.name}`);
3133
+ }
2922
3134
  }
2923
3135
  for (const collection of diff.collectionsToCreate) {
2924
3136
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2953,6 +3165,9 @@ function categorizeChangesBySeverity(diff, _config) {
2953
3165
  for (const rule of modification.rulesToUpdate) {
2954
3166
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2955
3167
  }
3168
+ if (modification.viewQueryUpdate) {
3169
+ nonDestructive.push(`Update view query: ${collectionName}`);
3170
+ }
2956
3171
  }
2957
3172
  return { destructive, nonDestructive };
2958
3173
  }
@@ -2981,12 +3196,11 @@ function filterDiff(diff, options) {
2981
3196
  });
2982
3197
  let collectionsToDelete = diff.collectionsToDelete;
2983
3198
  if (skipDestructive) {
2984
- collectionsToDelete = [];
2985
- } else {
2986
- collectionsToDelete = collectionsToDelete.filter((col) => {
2987
- return matchesPattern(col.name, patterns);
2988
- });
3199
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
2989
3200
  }
3201
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3202
+ return matchesPattern(col.name, patterns);
3203
+ });
2990
3204
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
2991
3205
  const collectionMatches = matchesPattern(mod.collection, patterns);
2992
3206
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -3011,6 +3225,7 @@ function filterDiff(diff, options) {
3011
3225
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
3012
3226
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
3013
3227
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3228
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
3014
3229
  return {
3015
3230
  ...mod,
3016
3231
  fieldsToAdd,
@@ -3019,10 +3234,11 @@ function filterDiff(diff, options) {
3019
3234
  indexesToAdd,
3020
3235
  indexesToRemove,
3021
3236
  rulesToUpdate,
3022
- permissionsToUpdate
3237
+ permissionsToUpdate,
3238
+ viewQueryUpdate
3023
3239
  };
3024
3240
  }).filter((mod) => {
3025
- 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;
3241
+ 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;
3026
3242
  });
3027
3243
  return {
3028
3244
  ...diff,
@@ -3034,7 +3250,7 @@ function filterDiff(diff, options) {
3034
3250
 
3035
3251
  // src/migration/diff/index.ts
3036
3252
  function hasChanges(modification) {
3037
- 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;
3253
+ 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;
3038
3254
  }
3039
3255
  function aggregateChanges(currentSchema, previousSnapshot, config) {
3040
3256
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3213,6 +3429,13 @@ function formatValue(value) {
3213
3429
  }
3214
3430
  return String(value);
3215
3431
  }
3432
+ function formatSqlTemplate(query, indent = " ") {
3433
+ const escaped = query.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
3434
+ const body = escaped.split("\n").map((line) => line.trim() === "" ? "" : `${indent}${line}`).join("\n");
3435
+ return `\`
3436
+ ${body}
3437
+ ${indent.slice(0, -2)}\``;
3438
+ }
3216
3439
  function getFieldConstructorName(fieldType) {
3217
3440
  const constructorMap = {
3218
3441
  text: "TextField",
@@ -3573,6 +3796,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3573
3796
  if (!rules) {
3574
3797
  return "";
3575
3798
  }
3799
+ if (collectionType === "view") {
3800
+ return [
3801
+ `"listRule": ${formatValue(rules.listRule ?? null)}`,
3802
+ `"viewRule": ${formatValue(rules.viewRule ?? null)}`,
3803
+ `"createRule": null`,
3804
+ `"updateRule": null`,
3805
+ `"deleteRule": null`
3806
+ ].join(",\n ");
3807
+ }
3576
3808
  const parts = [];
3577
3809
  if (rules.listRule !== void 0) {
3578
3810
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3598,6 +3830,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3598
3830
  if (!permissions) {
3599
3831
  return "";
3600
3832
  }
3833
+ if (collectionType === "view") {
3834
+ return [
3835
+ `"listRule": ${formatValue(permissions.listRule ?? null)}`,
3836
+ `"viewRule": ${formatValue(permissions.viewRule ?? null)}`,
3837
+ `"createRule": null`,
3838
+ `"updateRule": null`,
3839
+ `"deleteRule": null`
3840
+ ].join(",\n ");
3841
+ }
3601
3842
  const parts = [];
3602
3843
  if (permissions.listRule !== void 0) {
3603
3844
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3635,6 +3876,16 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3635
3876
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3636
3877
  return lines.join("\n");
3637
3878
  }
3879
+ function generateViewQueryUpdate(collectionName, viewQuery, varName, isLast = false, collectionIdMap) {
3880
+ const lines = [];
3881
+ const collectionVar = varName || `collection_${collectionName}_viewQuery`;
3882
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3883
+ lines.push(` unmarshal({`);
3884
+ lines.push(` "viewQuery": ${formatSqlTemplate(viewQuery, " ")},`);
3885
+ lines.push(` }, ${collectionVar})`);
3886
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3887
+ return lines.join("\n");
3888
+ }
3638
3889
  function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3639
3890
  const collectionVar = `collection_${collectionName}_${varSuffix}`;
3640
3891
  const lines = [];
@@ -3665,6 +3916,13 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3665
3916
  } else if (rulesCode) {
3666
3917
  lines.push(` ${rulesCode},`);
3667
3918
  }
3919
+ if (collection.type === "view") {
3920
+ lines.push(` "viewQuery": ${formatSqlTemplate(collection.viewQuery ?? "")},`);
3921
+ lines.push(` });`);
3922
+ lines.push(``);
3923
+ lines.push(isLast ? ` return app.save(${varName});` : ` app.save(${varName});`);
3924
+ return lines.join("\n");
3925
+ }
3668
3926
  const systemFieldNames = ["created", "updated", "id"];
3669
3927
  if (collection.type === "auth") {
3670
3928
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
@@ -3705,7 +3963,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3705
3963
  const modification = operation.modifications;
3706
3964
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3707
3965
  let operationCount = 0;
3708
- 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);
3966
+ 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);
3967
+ if (modification.viewQueryUpdate) {
3968
+ operationCount++;
3969
+ const isLast = operationCount === totalOperations;
3970
+ lines.push(
3971
+ generateViewQueryUpdate(
3972
+ collectionName,
3973
+ modification.viewQueryUpdate.newValue,
3974
+ void 0,
3975
+ isLast,
3976
+ collectionIdMap
3977
+ )
3978
+ );
3979
+ if (!isLast) lines.push("");
3980
+ }
3709
3981
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3710
3982
  const field = modification.fieldsToAdd[i];
3711
3983
  operationCount++;
@@ -3806,7 +4078,21 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3806
4078
  const modification = operation.modifications;
3807
4079
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3808
4080
  let operationCount = 0;
3809
- 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);
4081
+ 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);
4082
+ if (modification.viewQueryUpdate) {
4083
+ operationCount++;
4084
+ const isLast = operationCount === totalOperations;
4085
+ lines.push(
4086
+ generateViewQueryUpdate(
4087
+ collectionName,
4088
+ modification.viewQueryUpdate.oldValue ?? "",
4089
+ `collection_${collectionName}_revert_viewQuery`,
4090
+ isLast,
4091
+ collectionIdMap
4092
+ )
4093
+ );
4094
+ if (!isLast) lines.push("");
4095
+ }
3810
4096
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3811
4097
  operationCount++;
3812
4098
  const isLast = operationCount === totalOperations;
@@ -4079,6 +4365,9 @@ function generate(diff, config) {
4079
4365
  function detectCollectionDeletions(diff) {
4080
4366
  const changes = [];
4081
4367
  for (const collection of diff.collectionsToDelete) {
4368
+ if (collection.type === "view") {
4369
+ continue;
4370
+ }
4082
4371
  changes.push({
4083
4372
  type: "collection_deletion" /* COLLECTION_DELETION */,
4084
4373
  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
  }
@@ -4919,11 +5215,14 @@ var TypeGenerator = class {
4919
5215
  }
4920
5216
  generateCollectionType(collection) {
4921
5217
  const typeName = this.toPascalCase(collection.name);
5218
+ const isView = collection.type === "view";
4922
5219
  const lines = [];
4923
5220
  lines.push(`export interface ${typeName}Record {`);
4924
5221
  lines.push(` id: string;`);
4925
- lines.push(` created: string;`);
4926
- lines.push(` updated: string;`);
5222
+ if (!isView) {
5223
+ lines.push(` created: string;`);
5224
+ lines.push(` updated: string;`);
5225
+ }
4927
5226
  lines.push(` collectionId: string;`);
4928
5227
  lines.push(` collectionName: "${collection.name}";`);
4929
5228
  for (const field of collection.fields) {
@@ -4935,7 +5234,7 @@ var TypeGenerator = class {
4935
5234
  lines.push(`}`);
4936
5235
  lines.push(``);
4937
5236
  const expandFields = [];
4938
- for (const field of collection.fields) {
5237
+ for (const field of isView ? [] : collection.fields) {
4939
5238
  if (field.type === "relation" && field.relation) {
4940
5239
  const targetCollection = field.relation.collection;
4941
5240
  let targetType = "RecordModel";