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
@@ -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,27 @@ 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
+ }
2171
2314
  const unmarshalRegex = /unmarshal\s*\(/g;
2172
2315
  let unmarshalMatch;
2173
2316
  while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
@@ -2182,6 +2325,10 @@ function parseMigrationOperationsFromContent(content) {
2182
2325
  while (i < content.length && braceCount > 0) {
2183
2326
  const ch = content[i];
2184
2327
  const prev = i > 0 ? content[i - 1] : "";
2328
+ if (!inStr && ch === "`") {
2329
+ i = skipTemplateLiteral(content, i);
2330
+ continue;
2331
+ }
2185
2332
  if (!inStr && (ch === '"' || ch === "'")) {
2186
2333
  inStr = true;
2187
2334
  strChar = ch;
@@ -2215,6 +2362,22 @@ function parseMigrationOperationsFromContent(content) {
2215
2362
  getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2216
2363
  }
2217
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
+ }
2218
2381
  }
2219
2382
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2220
2383
  let match;
@@ -2345,6 +2508,10 @@ function parseMigrationOperations(migrationContent) {
2345
2508
  while (i < migrationContent.length && braceCount > 0) {
2346
2509
  const char = migrationContent[i];
2347
2510
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2511
+ if (!inString && char === "`") {
2512
+ i = skipTemplateLiteral(migrationContent, i);
2513
+ continue;
2514
+ }
2348
2515
  if (!inString && (char === '"' || char === "'")) {
2349
2516
  inString = true;
2350
2517
  stringChar = char;
@@ -2452,6 +2619,9 @@ function applyMigrationOperations(snapshot, operations) {
2452
2619
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2453
2620
  }
2454
2621
  }
2622
+ if (update.viewQuery !== void 0) {
2623
+ collection.viewQuery = update.viewQuery;
2624
+ }
2455
2625
  if (Object.keys(update.rulesToUpdate).length > 0) {
2456
2626
  if (!collection.rules) collection.rules = {};
2457
2627
  if (!collection.permissions) collection.permissions = {};
@@ -2915,7 +3085,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2915
3085
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2916
3086
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2917
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
+ }
2918
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
+ }
2919
3127
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2920
3128
  currentCollection,
2921
3129
  previousCollection,
@@ -2947,7 +3155,11 @@ function categorizeChangesBySeverity(diff, _config) {
2947
3155
  const destructive = [];
2948
3156
  const nonDestructive = [];
2949
3157
  for (const collection of diff.collectionsToDelete) {
2950
- 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
+ }
2951
3163
  }
2952
3164
  for (const collection of diff.collectionsToCreate) {
2953
3165
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2982,6 +3194,9 @@ function categorizeChangesBySeverity(diff, _config) {
2982
3194
  for (const rule of modification.rulesToUpdate) {
2983
3195
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2984
3196
  }
3197
+ if (modification.viewQueryUpdate) {
3198
+ nonDestructive.push(`Update view query: ${collectionName}`);
3199
+ }
2985
3200
  }
2986
3201
  return { destructive, nonDestructive };
2987
3202
  }
@@ -3010,12 +3225,11 @@ function filterDiff(diff, options) {
3010
3225
  });
3011
3226
  let collectionsToDelete = diff.collectionsToDelete;
3012
3227
  if (skipDestructive) {
3013
- collectionsToDelete = [];
3014
- } else {
3015
- collectionsToDelete = collectionsToDelete.filter((col) => {
3016
- return matchesPattern(col.name, patterns);
3017
- });
3228
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
3018
3229
  }
3230
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3231
+ return matchesPattern(col.name, patterns);
3232
+ });
3019
3233
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
3020
3234
  const collectionMatches = matchesPattern(mod.collection, patterns);
3021
3235
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -3040,6 +3254,7 @@ function filterDiff(diff, options) {
3040
3254
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
3041
3255
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
3042
3256
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3257
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
3043
3258
  return {
3044
3259
  ...mod,
3045
3260
  fieldsToAdd,
@@ -3048,10 +3263,11 @@ function filterDiff(diff, options) {
3048
3263
  indexesToAdd,
3049
3264
  indexesToRemove,
3050
3265
  rulesToUpdate,
3051
- permissionsToUpdate
3266
+ permissionsToUpdate,
3267
+ viewQueryUpdate
3052
3268
  };
3053
3269
  }).filter((mod) => {
3054
- 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;
3055
3271
  });
3056
3272
  return {
3057
3273
  ...diff,
@@ -3063,7 +3279,7 @@ function filterDiff(diff, options) {
3063
3279
 
3064
3280
  // src/migration/diff/index.ts
3065
3281
  function hasChanges(modification) {
3066
- 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;
3067
3283
  }
3068
3284
  function aggregateChanges(currentSchema, previousSnapshot, config) {
3069
3285
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3242,6 +3458,13 @@ function formatValue(value) {
3242
3458
  }
3243
3459
  return String(value);
3244
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
+ }
3245
3468
  function getFieldConstructorName(fieldType) {
3246
3469
  const constructorMap = {
3247
3470
  text: "TextField",
@@ -3602,6 +3825,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3602
3825
  if (!rules) {
3603
3826
  return "";
3604
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
+ }
3605
3837
  const parts = [];
3606
3838
  if (rules.listRule !== void 0) {
3607
3839
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3627,6 +3859,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3627
3859
  if (!permissions) {
3628
3860
  return "";
3629
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
+ }
3630
3871
  const parts = [];
3631
3872
  if (permissions.listRule !== void 0) {
3632
3873
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3664,6 +3905,16 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3664
3905
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3665
3906
  return lines.join("\n");
3666
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
+ }
3667
3918
  function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3668
3919
  const collectionVar = `collection_${collectionName}_${varSuffix}`;
3669
3920
  const lines = [];
@@ -3694,6 +3945,13 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3694
3945
  } else if (rulesCode) {
3695
3946
  lines.push(` ${rulesCode},`);
3696
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
+ }
3697
3955
  const systemFieldNames = ["created", "updated", "id"];
3698
3956
  if (collection.type === "auth") {
3699
3957
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
@@ -3734,7 +3992,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3734
3992
  const modification = operation.modifications;
3735
3993
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3736
3994
  let operationCount = 0;
3737
- 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);
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
+ }
3738
4010
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3739
4011
  const field = modification.fieldsToAdd[i];
3740
4012
  operationCount++;
@@ -3835,7 +4107,21 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3835
4107
  const modification = operation.modifications;
3836
4108
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3837
4109
  let operationCount = 0;
3838
- 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);
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
+ }
3839
4125
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3840
4126
  operationCount++;
3841
4127
  const isLast = operationCount === totalOperations;
@@ -4108,6 +4394,9 @@ function generate(diff, config) {
4108
4394
  function detectCollectionDeletions(diff) {
4109
4395
  const changes = [];
4110
4396
  for (const collection of diff.collectionsToDelete) {
4397
+ if (collection.type === "view") {
4398
+ continue;
4399
+ }
4111
4400
  changes.push({
4112
4401
  type: "collection_deletion" /* COLLECTION_DELETION */,
4113
4402
  description: `Delete collection: ${collection.name}`,
@@ -4518,7 +4807,11 @@ function formatChangeSummary(diff) {
4518
4807
  lines.push(chalk__default.default.green.bold(`\u2713 ${totalCollectionsToCreate} collection(s) to create:`));
4519
4808
  for (const collection of diff.collectionsToCreate) {
4520
4809
  lines.push(chalk__default.default.green(` + ${collection.name} (${collection.type})`));
4521
- 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
+ }
4522
4815
  }
4523
4816
  lines.push("");
4524
4817
  }
@@ -4563,6 +4856,9 @@ function formatChangeSummary(diff) {
4563
4856
  if (modification.rulesToUpdate.length > 0) {
4564
4857
  lines.push(chalk__default.default.yellow(` ~ ${modification.rulesToUpdate.length} rule(s) to update`));
4565
4858
  }
4859
+ if (modification.viewQueryUpdate) {
4860
+ lines.push(chalk__default.default.yellow(` ~ view query to update`));
4861
+ }
4566
4862
  lines.push("");
4567
4863
  }
4568
4864
  }
@@ -4948,11 +5244,14 @@ var TypeGenerator = class {
4948
5244
  }
4949
5245
  generateCollectionType(collection) {
4950
5246
  const typeName = this.toPascalCase(collection.name);
5247
+ const isView = collection.type === "view";
4951
5248
  const lines = [];
4952
5249
  lines.push(`export interface ${typeName}Record {`);
4953
5250
  lines.push(` id: string;`);
4954
- lines.push(` created: string;`);
4955
- lines.push(` updated: string;`);
5251
+ if (!isView) {
5252
+ lines.push(` created: string;`);
5253
+ lines.push(` updated: string;`);
5254
+ }
4956
5255
  lines.push(` collectionId: string;`);
4957
5256
  lines.push(` collectionName: "${collection.name}";`);
4958
5257
  for (const field of collection.fields) {
@@ -4964,7 +5263,7 @@ var TypeGenerator = class {
4964
5263
  lines.push(`}`);
4965
5264
  lines.push(``);
4966
5265
  const expandFields = [];
4967
- for (const field of collection.fields) {
5266
+ for (const field of isView ? [] : collection.fields) {
4968
5267
  if (field.type === "relation" && field.relation) {
4969
5268
  const targetCollection = field.relation.collection;
4970
5269
  let targetType = "RecordModel";