pocketbase-zod-schema 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli/index.cjs +449 -50
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.d.cts +1 -1
  5. package/dist/cli/index.d.ts +1 -1
  6. package/dist/cli/index.js +449 -50
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/migrate.cjs +455 -53
  9. package/dist/cli/migrate.cjs.map +1 -1
  10. package/dist/cli/migrate.js +455 -53
  11. package/dist/cli/migrate.js.map +1 -1
  12. package/dist/cli/utils/index.cjs +8 -1
  13. package/dist/cli/utils/index.cjs.map +1 -1
  14. package/dist/cli/utils/index.d.cts +1 -1
  15. package/dist/cli/utils/index.d.ts +1 -1
  16. package/dist/cli/utils/index.js +8 -1
  17. package/dist/cli/utils/index.js.map +1 -1
  18. package/dist/index.cjs +90 -1
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +87 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/migration/analyzer.cjs +87 -14
  25. package/dist/migration/analyzer.cjs.map +1 -1
  26. package/dist/migration/analyzer.d.cts +12 -4
  27. package/dist/migration/analyzer.d.ts +12 -4
  28. package/dist/migration/analyzer.js +87 -15
  29. package/dist/migration/analyzer.js.map +1 -1
  30. package/dist/migration/diff.cjs +64 -9
  31. package/dist/migration/diff.cjs.map +1 -1
  32. package/dist/migration/diff.d.cts +12 -2
  33. package/dist/migration/diff.d.ts +12 -2
  34. package/dist/migration/diff.js +64 -10
  35. package/dist/migration/diff.js.map +1 -1
  36. package/dist/migration/generator.cjs +206 -39
  37. package/dist/migration/generator.cjs.map +1 -1
  38. package/dist/migration/generator.d.cts +54 -2
  39. package/dist/migration/generator.d.ts +54 -2
  40. package/dist/migration/generator.js +203 -40
  41. package/dist/migration/generator.js.map +1 -1
  42. package/dist/migration/index.cjs +501 -63
  43. package/dist/migration/index.cjs.map +1 -1
  44. package/dist/migration/index.d.cts +2 -2
  45. package/dist/migration/index.d.ts +2 -2
  46. package/dist/migration/index.js +501 -63
  47. package/dist/migration/index.js.map +1 -1
  48. package/dist/migration/snapshot.cjs +147 -1
  49. package/dist/migration/snapshot.cjs.map +1 -1
  50. package/dist/migration/snapshot.d.cts +3 -1
  51. package/dist/migration/snapshot.d.ts +3 -1
  52. package/dist/migration/snapshot.js +147 -1
  53. package/dist/migration/snapshot.js.map +1 -1
  54. package/dist/migration/utils/index.d.cts +1 -1
  55. package/dist/migration/utils/index.d.ts +1 -1
  56. package/dist/schema.cjs +90 -1
  57. package/dist/schema.cjs.map +1 -1
  58. package/dist/schema.d.cts +128 -2
  59. package/dist/schema.d.ts +128 -2
  60. package/dist/schema.js +87 -2
  61. package/dist/schema.js.map +1 -1
  62. package/dist/server.cjs +545 -65
  63. package/dist/server.cjs.map +1 -1
  64. package/dist/server.d.cts +2 -2
  65. package/dist/server.d.ts +2 -2
  66. package/dist/server.js +542 -66
  67. package/dist/server.js.map +1 -1
  68. package/dist/{types-CWHV6ATd.d.cts → types-CnzfX6JH.d.cts} +20 -2
  69. package/dist/{types-C2nGWHLV.d.ts → types-Do3jyFBm.d.ts} +20 -2
  70. package/package.json +1 -1
@@ -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,95 @@ 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
+ }
2285
+ const unmarshalRegex = /unmarshal\s*\(/g;
2286
+ let unmarshalMatch;
2287
+ while ((unmarshalMatch = unmarshalRegex.exec(content)) !== null) {
2288
+ let i = unmarshalMatch.index + unmarshalMatch[0].length;
2289
+ while (i < content.length && /\s/.test(content[i])) i++;
2290
+ if (content[i] !== "{") continue;
2291
+ const objStart = i;
2292
+ let braceCount = 1;
2293
+ let inStr = false;
2294
+ let strChar = null;
2295
+ i++;
2296
+ while (i < content.length && braceCount > 0) {
2297
+ const ch = content[i];
2298
+ const prev = i > 0 ? content[i - 1] : "";
2299
+ if (!inStr && ch === "`") {
2300
+ i = skipTemplateLiteral(content, i);
2301
+ continue;
2302
+ }
2303
+ if (!inStr && (ch === '"' || ch === "'")) {
2304
+ inStr = true;
2305
+ strChar = ch;
2306
+ } else if (inStr && ch === strChar && prev !== "\\") {
2307
+ inStr = false;
2308
+ strChar = null;
2309
+ }
2310
+ if (!inStr) {
2311
+ if (ch === "{") braceCount++;
2312
+ if (ch === "}") braceCount--;
2313
+ }
2314
+ i++;
2315
+ }
2316
+ if (braceCount !== 0) continue;
2317
+ const objStr = content.substring(objStart, i);
2318
+ while (i < content.length && /[\s,]/.test(content[i])) i++;
2319
+ const varStart = i;
2320
+ while (i < content.length && /\w/.test(content[i])) i++;
2321
+ const colVarName = content.substring(varStart, i);
2322
+ const colInfo = variables.get(colVarName);
2323
+ if (!colInfo || colInfo.type !== "collection") continue;
2324
+ const keyValRegex = /"(\w+Rule)"\s*:\s*([^,}\n]+)/g;
2325
+ let kvMatch;
2326
+ while ((kvMatch = keyValRegex.exec(objStr)) !== null) {
2327
+ const ruleKey = kvMatch[1];
2328
+ const valStr = kvMatch[2].trim();
2329
+ try {
2330
+ const value = new Function("app", `return ${valStr}`)(mockApp);
2331
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = value;
2332
+ } catch {
2333
+ getUpdate(colInfo.name).rulesToUpdate[ruleKey] = valStr;
2334
+ }
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
+ }
2352
+ }
2142
2353
  const idxPushRegex = /(\w+)\.indexes\.push\s*\(/g;
2143
2354
  let match;
2144
2355
  while ((match = idxPushRegex.exec(content)) !== null) {
@@ -2268,6 +2479,10 @@ function parseMigrationOperations(migrationContent) {
2268
2479
  while (i < migrationContent.length && braceCount > 0) {
2269
2480
  const char = migrationContent[i];
2270
2481
  const prevChar = i > 0 ? migrationContent[i - 1] : "";
2482
+ if (!inString && char === "`") {
2483
+ i = skipTemplateLiteral(migrationContent, i);
2484
+ continue;
2485
+ }
2271
2486
  if (!inString && (char === '"' || char === "'")) {
2272
2487
  inString = true;
2273
2488
  stringChar = char;
@@ -2375,6 +2590,9 @@ function applyMigrationOperations(snapshot, operations) {
2375
2590
  collection.indexes = collection.indexes.filter((idx) => !update.indexesToRemove.includes(idx));
2376
2591
  }
2377
2592
  }
2593
+ if (update.viewQuery !== void 0) {
2594
+ collection.viewQuery = update.viewQuery;
2595
+ }
2378
2596
  if (Object.keys(update.rulesToUpdate).length > 0) {
2379
2597
  if (!collection.rules) collection.rules = {};
2380
2598
  if (!collection.permissions) collection.permissions = {};
@@ -2838,7 +3056,45 @@ function compareCollectionFields(currentCollection, previousCollection, config,
2838
3056
  fieldsToRemove = fieldsToRemove.filter((_, index) => !processedRemoveIndices.has(index));
2839
3057
  return { fieldsToAdd, fieldsToRemove, fieldsToModify };
2840
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
+ }
2841
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
+ }
2842
3098
  const { fieldsToAdd, fieldsToRemove, fieldsToModify } = compareCollectionFields(
2843
3099
  currentCollection,
2844
3100
  previousCollection,
@@ -2870,7 +3126,11 @@ function categorizeChangesBySeverity(diff, _config) {
2870
3126
  const destructive = [];
2871
3127
  const nonDestructive = [];
2872
3128
  for (const collection of diff.collectionsToDelete) {
2873
- 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
+ }
2874
3134
  }
2875
3135
  for (const collection of diff.collectionsToCreate) {
2876
3136
  nonDestructive.push(`Create collection: ${collection.name}`);
@@ -2905,6 +3165,9 @@ function categorizeChangesBySeverity(diff, _config) {
2905
3165
  for (const rule of modification.rulesToUpdate) {
2906
3166
  nonDestructive.push(`Update rule: ${collectionName}.${rule.ruleType}`);
2907
3167
  }
3168
+ if (modification.viewQueryUpdate) {
3169
+ nonDestructive.push(`Update view query: ${collectionName}`);
3170
+ }
2908
3171
  }
2909
3172
  return { destructive, nonDestructive };
2910
3173
  }
@@ -2933,12 +3196,11 @@ function filterDiff(diff, options) {
2933
3196
  });
2934
3197
  let collectionsToDelete = diff.collectionsToDelete;
2935
3198
  if (skipDestructive) {
2936
- collectionsToDelete = [];
2937
- } else {
2938
- collectionsToDelete = collectionsToDelete.filter((col) => {
2939
- return matchesPattern(col.name, patterns);
2940
- });
3199
+ collectionsToDelete = collectionsToDelete.filter((col) => col.type === "view");
2941
3200
  }
3201
+ collectionsToDelete = collectionsToDelete.filter((col) => {
3202
+ return matchesPattern(col.name, patterns);
3203
+ });
2942
3204
  const collectionsToModify = diff.collectionsToModify.map((mod) => {
2943
3205
  const collectionMatches = matchesPattern(mod.collection, patterns);
2944
3206
  const fieldsToAdd = mod.fieldsToAdd.filter((field) => {
@@ -2963,6 +3225,7 @@ function filterDiff(diff, options) {
2963
3225
  const indexesToRemove = collectionMatches ? mod.indexesToRemove : [];
2964
3226
  const rulesToUpdate = collectionMatches ? mod.rulesToUpdate : [];
2965
3227
  const permissionsToUpdate = collectionMatches ? mod.permissionsToUpdate : [];
3228
+ const viewQueryUpdate = collectionMatches ? mod.viewQueryUpdate : void 0;
2966
3229
  return {
2967
3230
  ...mod,
2968
3231
  fieldsToAdd,
@@ -2971,10 +3234,11 @@ function filterDiff(diff, options) {
2971
3234
  indexesToAdd,
2972
3235
  indexesToRemove,
2973
3236
  rulesToUpdate,
2974
- permissionsToUpdate
3237
+ permissionsToUpdate,
3238
+ viewQueryUpdate
2975
3239
  };
2976
3240
  }).filter((mod) => {
2977
- 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;
2978
3242
  });
2979
3243
  return {
2980
3244
  ...diff,
@@ -2986,7 +3250,7 @@ function filterDiff(diff, options) {
2986
3250
 
2987
3251
  // src/migration/diff/index.ts
2988
3252
  function hasChanges(modification) {
2989
- 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;
2990
3254
  }
2991
3255
  function aggregateChanges(currentSchema, previousSnapshot, config) {
2992
3256
  const collectionIdToName = /* @__PURE__ */ new Map();
@@ -3165,6 +3429,13 @@ function formatValue(value) {
3165
3429
  }
3166
3430
  return String(value);
3167
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
+ }
3168
3439
  function getFieldConstructorName(fieldType) {
3169
3440
  const constructorMap = {
3170
3441
  text: "TextField",
@@ -3203,6 +3474,36 @@ function getSystemFields() {
3203
3474
  }
3204
3475
  ];
3205
3476
  }
3477
+ function getSystemTimestampFields() {
3478
+ return [
3479
+ {
3480
+ name: "created",
3481
+ id: "autodate2990389176",
3482
+ type: "autodate",
3483
+ required: false,
3484
+ options: {
3485
+ hidden: false,
3486
+ onCreate: true,
3487
+ onUpdate: false,
3488
+ presentable: false,
3489
+ system: true
3490
+ }
3491
+ },
3492
+ {
3493
+ name: "updated",
3494
+ id: "autodate3332085495",
3495
+ type: "autodate",
3496
+ required: false,
3497
+ options: {
3498
+ hidden: false,
3499
+ onCreate: true,
3500
+ onUpdate: true,
3501
+ presentable: false,
3502
+ system: true
3503
+ }
3504
+ }
3505
+ ];
3506
+ }
3206
3507
  function getAuthSystemFields() {
3207
3508
  return [
3208
3509
  {
@@ -3495,6 +3796,15 @@ function generateCollectionRules(rules, collectionType = "base") {
3495
3796
  if (!rules) {
3496
3797
  return "";
3497
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
+ }
3498
3808
  const parts = [];
3499
3809
  if (rules.listRule !== void 0) {
3500
3810
  parts.push(`"listRule": ${formatValue(rules.listRule)}`);
@@ -3520,6 +3830,15 @@ function generateCollectionPermissions(permissions, collectionType = "base") {
3520
3830
  if (!permissions) {
3521
3831
  return "";
3522
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
+ }
3523
3842
  const parts = [];
3524
3843
  if (permissions.listRule !== void 0) {
3525
3844
  parts.push(`"listRule": ${formatValue(permissions.listRule)}`);
@@ -3557,6 +3876,28 @@ function generatePermissionUpdate(collectionName, ruleType, newValue, varName, i
3557
3876
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3558
3877
  return lines.join("\n");
3559
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
+ }
3889
+ function generateGroupedRuleUpdates(collectionName, entries, varSuffix, isLast = false, collectionIdMap) {
3890
+ const collectionVar = `collection_${collectionName}_${varSuffix}`;
3891
+ const lines = [];
3892
+ lines.push(` const ${collectionVar} = ${generateFindCollectionCode(collectionName, collectionIdMap)};`);
3893
+ lines.push(` unmarshal({`);
3894
+ for (const entry of entries) {
3895
+ lines.push(` "${entry.ruleType}": ${formatValue(entry.value)},`);
3896
+ }
3897
+ lines.push(` }, ${collectionVar})`);
3898
+ lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
3899
+ return lines.join("\n");
3900
+ }
3560
3901
 
3561
3902
  // src/migration/generator/collections.ts
3562
3903
  function generateCollectionCreation(collection, varName = "collection", isLast = false, collectionIdMap) {
@@ -3575,16 +3916,24 @@ function generateCollectionCreation(collection, varName = "collection", isLast =
3575
3916
  } else if (rulesCode) {
3576
3917
  lines.push(` ${rulesCode},`);
3577
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
+ }
3578
3926
  const systemFieldNames = ["created", "updated", "id"];
3579
3927
  if (collection.type === "auth") {
3580
3928
  systemFieldNames.push(...getAuthSystemFields().map((f) => f.name));
3581
3929
  }
3582
3930
  const userFields = collection.fields.filter((f) => !systemFieldNames.includes(f.name));
3583
- const allFields = [...getSystemFields().filter((f) => f.name === "id")];
3931
+ const allFields = [...getSystemFields()];
3584
3932
  if (collection.type === "auth") {
3585
3933
  allFields.push(...getAuthSystemFields());
3586
3934
  }
3587
3935
  allFields.push(...userFields);
3936
+ allFields.push(...getSystemTimestampFields());
3588
3937
  lines.push(` "fields": ${generateFieldsArray(allFields, collectionIdMap)},`);
3589
3938
  let allIndexes = [...collection.indexes || []];
3590
3939
  if (collection.type === "auth") {
@@ -3614,7 +3963,21 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3614
3963
  const modification = operation.modifications;
3615
3964
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3616
3965
  let operationCount = 0;
3617
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
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
+ }
3618
3981
  for (let i = 0; i < modification.fieldsToAdd.length; i++) {
3619
3982
  const field = modification.fieldsToAdd[i];
3620
3983
  operationCount++;
@@ -3654,23 +4017,29 @@ function generateOperationUpMigration(operation, collectionIdMap) {
3654
4017
  if (!isLast) lines.push("");
3655
4018
  }
3656
4019
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3657
- for (const permission of modification.permissionsToUpdate) {
3658
- operationCount++;
4020
+ operationCount++;
4021
+ const isLast = operationCount === totalOperations;
4022
+ if (modification.permissionsToUpdate.length >= 2) {
4023
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.newValue }));
4024
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4025
+ } else {
4026
+ const permission = modification.permissionsToUpdate[0];
3659
4027
  const varName = `collection_${collectionName}_perm_${permission.ruleType}`;
3660
- const isLast = operationCount === totalOperations;
3661
- lines.push(
3662
- generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap)
3663
- );
3664
- if (!isLast) lines.push("");
4028
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.newValue, varName, isLast, collectionIdMap));
3665
4029
  }
4030
+ if (!isLast) lines.push("");
3666
4031
  } else if (modification.rulesToUpdate.length > 0) {
3667
- for (const rule of modification.rulesToUpdate) {
3668
- operationCount++;
4032
+ operationCount++;
4033
+ const isLast = operationCount === totalOperations;
4034
+ if (modification.rulesToUpdate.length >= 2) {
4035
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.newValue }));
4036
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "rules", isLast, collectionIdMap));
4037
+ } else {
4038
+ const rule = modification.rulesToUpdate[0];
3669
4039
  const varName = `collection_${collectionName}_rule_${rule.ruleType}`;
3670
- const isLast = operationCount === totalOperations;
3671
4040
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.newValue, varName, isLast, collectionIdMap));
3672
- if (!isLast) lines.push("");
3673
4041
  }
4042
+ if (!isLast) lines.push("");
3674
4043
  }
3675
4044
  } else if (operation.type === "delete") {
3676
4045
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection.name;
@@ -3709,25 +4078,45 @@ function generateOperationDownMigration(operation, collectionIdMap) {
3709
4078
  const modification = operation.modifications;
3710
4079
  const collectionName = typeof operation.collection === "string" ? operation.collection : operation.collection?.name ?? modification.collection;
3711
4080
  let operationCount = 0;
3712
- const totalOperations = modification.fieldsToAdd.length + modification.fieldsToModify.length + modification.fieldsToRemove.length + modification.indexesToAdd.length + modification.indexesToRemove.length + modification.rulesToUpdate.length + modification.permissionsToUpdate.length;
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
+ }
3713
4096
  if (modification.permissionsToUpdate && modification.permissionsToUpdate.length > 0) {
3714
- for (const permission of modification.permissionsToUpdate) {
3715
- operationCount++;
4097
+ operationCount++;
4098
+ const isLast = operationCount === totalOperations;
4099
+ if (modification.permissionsToUpdate.length >= 2) {
4100
+ const entries = modification.permissionsToUpdate.map((p) => ({ ruleType: p.ruleType, value: p.oldValue }));
4101
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4102
+ } else {
4103
+ const permission = modification.permissionsToUpdate[0];
3716
4104
  const varName = `collection_${collectionName}_revert_perm_${permission.ruleType}`;
3717
- const isLast = operationCount === totalOperations;
3718
- lines.push(
3719
- generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap)
3720
- );
3721
- if (!isLast) lines.push("");
4105
+ lines.push(generatePermissionUpdate(collectionName, permission.ruleType, permission.oldValue, varName, isLast, collectionIdMap));
3722
4106
  }
4107
+ if (!isLast) lines.push("");
3723
4108
  } else if (modification.rulesToUpdate.length > 0) {
3724
- for (const rule of modification.rulesToUpdate) {
3725
- operationCount++;
4109
+ operationCount++;
4110
+ const isLast = operationCount === totalOperations;
4111
+ if (modification.rulesToUpdate.length >= 2) {
4112
+ const entries = modification.rulesToUpdate.map((r) => ({ ruleType: r.ruleType, value: r.oldValue }));
4113
+ lines.push(generateGroupedRuleUpdates(collectionName, entries, "revert_rules", isLast, collectionIdMap));
4114
+ } else {
4115
+ const rule = modification.rulesToUpdate[0];
3726
4116
  const varName = `collection_${collectionName}_revert_rule_${rule.ruleType}`;
3727
- const isLast = operationCount === totalOperations;
3728
4117
  lines.push(generateRuleUpdate(collectionName, rule.ruleType, rule.oldValue, varName, isLast, collectionIdMap));
3729
- if (!isLast) lines.push("");
3730
4118
  }
4119
+ if (!isLast) lines.push("");
3731
4120
  }
3732
4121
  for (let i = 0; i < modification.indexesToRemove.length; i++) {
3733
4122
  operationCount++;
@@ -3976,6 +4365,9 @@ function generate(diff, config) {
3976
4365
  function detectCollectionDeletions(diff) {
3977
4366
  const changes = [];
3978
4367
  for (const collection of diff.collectionsToDelete) {
4368
+ if (collection.type === "view") {
4369
+ continue;
4370
+ }
3979
4371
  changes.push({
3980
4372
  type: "collection_deletion" /* COLLECTION_DELETION */,
3981
4373
  description: `Delete collection: ${collection.name}`,
@@ -4386,7 +4778,11 @@ function formatChangeSummary(diff) {
4386
4778
  lines.push(chalk.green.bold(`\u2713 ${totalCollectionsToCreate} collection(s) to create:`));
4387
4779
  for (const collection of diff.collectionsToCreate) {
4388
4780
  lines.push(chalk.green(` + ${collection.name} (${collection.type})`));
4389
- 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
+ }
4390
4786
  }
4391
4787
  lines.push("");
4392
4788
  }
@@ -4431,6 +4827,9 @@ function formatChangeSummary(diff) {
4431
4827
  if (modification.rulesToUpdate.length > 0) {
4432
4828
  lines.push(chalk.yellow(` ~ ${modification.rulesToUpdate.length} rule(s) to update`));
4433
4829
  }
4830
+ if (modification.viewQueryUpdate) {
4831
+ lines.push(chalk.yellow(` ~ view query to update`));
4832
+ }
4434
4833
  lines.push("");
4435
4834
  }
4436
4835
  }
@@ -4816,11 +5215,14 @@ var TypeGenerator = class {
4816
5215
  }
4817
5216
  generateCollectionType(collection) {
4818
5217
  const typeName = this.toPascalCase(collection.name);
5218
+ const isView = collection.type === "view";
4819
5219
  const lines = [];
4820
5220
  lines.push(`export interface ${typeName}Record {`);
4821
5221
  lines.push(` id: string;`);
4822
- lines.push(` created: string;`);
4823
- lines.push(` updated: string;`);
5222
+ if (!isView) {
5223
+ lines.push(` created: string;`);
5224
+ lines.push(` updated: string;`);
5225
+ }
4824
5226
  lines.push(` collectionId: string;`);
4825
5227
  lines.push(` collectionName: "${collection.name}";`);
4826
5228
  for (const field of collection.fields) {
@@ -4832,7 +5234,7 @@ var TypeGenerator = class {
4832
5234
  lines.push(`}`);
4833
5235
  lines.push(``);
4834
5236
  const expandFields = [];
4835
- for (const field of collection.fields) {
5237
+ for (const field of isView ? [] : collection.fields) {
4836
5238
  if (field.type === "relation" && field.relation) {
4837
5239
  const targetCollection = field.relation.collection;
4838
5240
  let targetType = "RecordModel";