pocketbase-zod-schema 0.2.3 → 0.2.5

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 (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +209 -24
  3. package/dist/cli/index.cjs +34 -0
  4. package/dist/cli/index.cjs.map +1 -1
  5. package/dist/cli/index.d.cts +3 -1
  6. package/dist/cli/index.d.ts +3 -1
  7. package/dist/cli/index.js +34 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/migrate.cjs +34 -0
  10. package/dist/cli/migrate.cjs.map +1 -1
  11. package/dist/cli/migrate.js +34 -0
  12. package/dist/cli/migrate.js.map +1 -1
  13. package/dist/cli/utils/index.d.cts +3 -1
  14. package/dist/cli/utils/index.d.ts +3 -1
  15. package/dist/fields-YjcpBXVp.d.cts +348 -0
  16. package/dist/fields-YjcpBXVp.d.ts +348 -0
  17. package/dist/index.cjs +833 -694
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -4
  20. package/dist/index.d.ts +3 -4
  21. package/dist/index.js +818 -687
  22. package/dist/index.js.map +1 -1
  23. package/dist/migration/analyzer.cjs +34 -0
  24. package/dist/migration/analyzer.cjs.map +1 -1
  25. package/dist/migration/analyzer.d.cts +2 -1
  26. package/dist/migration/analyzer.d.ts +2 -1
  27. package/dist/migration/analyzer.js +34 -0
  28. package/dist/migration/analyzer.js.map +1 -1
  29. package/dist/migration/diff.d.cts +3 -1
  30. package/dist/migration/diff.d.ts +3 -1
  31. package/dist/migration/generator.d.cts +3 -1
  32. package/dist/migration/generator.d.ts +3 -1
  33. package/dist/migration/index.cjs +36 -0
  34. package/dist/migration/index.cjs.map +1 -1
  35. package/dist/migration/index.d.cts +2 -1
  36. package/dist/migration/index.d.ts +2 -1
  37. package/dist/migration/index.js +35 -1
  38. package/dist/migration/index.js.map +1 -1
  39. package/dist/migration/snapshot.cjs.map +1 -1
  40. package/dist/migration/snapshot.d.cts +3 -1
  41. package/dist/migration/snapshot.d.ts +3 -1
  42. package/dist/migration/snapshot.js.map +1 -1
  43. package/dist/migration/utils/index.cjs +16 -0
  44. package/dist/migration/utils/index.cjs.map +1 -1
  45. package/dist/migration/utils/index.d.cts +2 -2
  46. package/dist/migration/utils/index.d.ts +2 -2
  47. package/dist/migration/utils/index.js +15 -1
  48. package/dist/migration/utils/index.js.map +1 -1
  49. package/dist/mutator.cjs +2 -98
  50. package/dist/mutator.cjs.map +1 -1
  51. package/dist/mutator.d.cts +4 -31
  52. package/dist/mutator.d.ts +4 -31
  53. package/dist/mutator.js +2 -98
  54. package/dist/mutator.js.map +1 -1
  55. package/dist/schema.cjs +200 -78
  56. package/dist/schema.cjs.map +1 -1
  57. package/dist/schema.d.cts +1 -1
  58. package/dist/schema.d.ts +1 -1
  59. package/dist/schema.js +186 -72
  60. package/dist/schema.js.map +1 -1
  61. package/dist/{types-z1Dkjg8m.d.ts → types-LFBGHl9Y.d.ts} +2 -2
  62. package/dist/{types-BbTgmg6H.d.cts → types-mhQXWNi3.d.cts} +2 -2
  63. package/package.json +1 -6
  64. package/dist/types.cjs +0 -4
  65. package/dist/types.cjs.map +0 -1
  66. package/dist/types.d.cts +0 -14
  67. package/dist/types.d.ts +0 -14
  68. package/dist/types.js +0 -3
  69. package/dist/types.js.map +0 -1
  70. package/dist/user-BnFWg5tw.d.cts +0 -161
  71. package/dist/user-BnFWg5tw.d.ts +0 -161
package/dist/index.js CHANGED
@@ -15,6 +15,333 @@ var StatusEnum = z.enum([
15
15
  "fail"
16
16
  // Failed project at any stage
17
17
  ]);
18
+ var BaseMutator = class {
19
+ pb;
20
+ // Define a default property that subclasses will override
21
+ options = {
22
+ expand: [],
23
+ filter: [],
24
+ sort: []
25
+ };
26
+ constructor(pb, options) {
27
+ this.pb = pb;
28
+ this.initializeOptions();
29
+ if (options) {
30
+ this.overrideOptions(options);
31
+ }
32
+ }
33
+ initializeOptions() {
34
+ this.options = this.setDefaults();
35
+ }
36
+ /**
37
+ * Initialize options with class-specific defaults
38
+ * Subclasses should override this instead of directly setting options
39
+ */
40
+ setDefaults() {
41
+ return {
42
+ expand: [],
43
+ filter: [],
44
+ sort: []
45
+ };
46
+ }
47
+ /**
48
+ * Merge provided options with current options
49
+ */
50
+ overrideOptions(newOptions) {
51
+ if (newOptions.expand !== void 0) {
52
+ this.options.expand = newOptions.expand;
53
+ }
54
+ if (newOptions.filter !== void 0) {
55
+ this.options.filter = newOptions.filter;
56
+ }
57
+ if (newOptions.sort !== void 0) {
58
+ this.options.sort = newOptions.sort;
59
+ }
60
+ }
61
+ toSnakeCase(str) {
62
+ return str.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
63
+ }
64
+ /**
65
+ * Create a new entity
66
+ */
67
+ async create(input) {
68
+ try {
69
+ const data = await this.validateInput(input);
70
+ const record = await this.entityCreate(data);
71
+ return await this.processRecord(record);
72
+ } catch (error) {
73
+ return this.errorWrapper(error);
74
+ }
75
+ }
76
+ /**
77
+ * Update an existing entity
78
+ */
79
+ async update(id, input) {
80
+ try {
81
+ const record = await this.entityUpdate(id, input);
82
+ return await this.processRecord(record);
83
+ } catch (error) {
84
+ return this.errorWrapper(error);
85
+ }
86
+ }
87
+ /**
88
+ * Create or update entity (upsert)
89
+ */
90
+ async upsert(input) {
91
+ if (input?.id) {
92
+ return await this.update(input.id, input);
93
+ }
94
+ return await this.create(input);
95
+ }
96
+ /**
97
+ * Get entity by ID
98
+ */
99
+ async getById(id, expand) {
100
+ try {
101
+ const record = await this.entityGetById(id, expand);
102
+ return await this.processRecord(record);
103
+ } catch (error) {
104
+ return this.handleError(error, { allowNotFound: true });
105
+ }
106
+ }
107
+ /**
108
+ * Get first entity by filter
109
+ */
110
+ async getFirstByFilter(filter, expand, sort) {
111
+ try {
112
+ const record = await this.entityGetFirstByFilter(filter, expand, sort);
113
+ return await this.processRecord(record);
114
+ } catch (error) {
115
+ return this.handleError(error, { allowNotFound: true });
116
+ }
117
+ }
118
+ /**
119
+ * Get list of entities
120
+ */
121
+ async getList(page = 1, perPage = 100, filter, sort, expand) {
122
+ try {
123
+ const result = await this.entityGetList(page, perPage, filter, sort, expand);
124
+ return await this.processListResult(result);
125
+ } catch (error) {
126
+ return this.errorWrapper(error);
127
+ }
128
+ }
129
+ /**
130
+ * Delete entity by ID
131
+ */
132
+ async delete(id) {
133
+ try {
134
+ return await this.entityDelete(id);
135
+ } catch (error) {
136
+ return this.handleError(error, { returnValue: false });
137
+ }
138
+ }
139
+ /**
140
+ * Process a single record before returning it
141
+ * Can be overridden to handle special cases like mapped entities
142
+ */
143
+ async processRecord(record) {
144
+ return record;
145
+ }
146
+ /**
147
+ * Process a list result before returning it
148
+ * Can be overridden to handle special cases like mapped entities
149
+ */
150
+ async processListResult(result) {
151
+ const processedItems = await Promise.all(result.items.map((item) => this.processRecord(item)));
152
+ return {
153
+ ...result,
154
+ items: processedItems
155
+ };
156
+ }
157
+ /**
158
+ * Prepare expand parameter
159
+ * Combines default expands with provided expands
160
+ */
161
+ prepareExpand(expand) {
162
+ if (!this.options.expand.length && !expand) {
163
+ return void 0;
164
+ }
165
+ let expandArray = [...this.options.expand];
166
+ if (expand) {
167
+ if (typeof expand === "string") {
168
+ expandArray = expandArray.concat(expand.split(",").map((e) => e.trim()));
169
+ } else {
170
+ expandArray = expandArray.concat(expand);
171
+ }
172
+ }
173
+ const uniqueExpands = [...new Set(expandArray)].filter((e) => e !== "" && e !== void 0);
174
+ if (!uniqueExpands.length) {
175
+ return void 0;
176
+ }
177
+ return uniqueExpands.join(",");
178
+ }
179
+ /**
180
+ * Prepare filter parameter
181
+ * Combines default filters with provided filters
182
+ */
183
+ prepareFilter(filter) {
184
+ if (!this.options.filter.length && !filter) {
185
+ return void 0;
186
+ }
187
+ let filterArray = [...this.options.filter];
188
+ if (filter) {
189
+ if (typeof filter === "string") {
190
+ if (filter) filterArray.push(filter);
191
+ } else {
192
+ filterArray = filterArray.concat(filter);
193
+ }
194
+ }
195
+ const validFilters = filterArray.filter((f) => f !== "" && f !== void 0);
196
+ if (!validFilters.length) {
197
+ return void 0;
198
+ }
199
+ return validFilters.join("&&");
200
+ }
201
+ /**
202
+ * Prepare sort parameter
203
+ * Uses provided sort or falls back to default sort
204
+ */
205
+ prepareSort(sort) {
206
+ if (sort && sort !== "") {
207
+ return sort;
208
+ }
209
+ if (this.options.sort.length) {
210
+ const validSorts = this.options.sort.filter((s) => s !== "" && s !== void 0);
211
+ if (validSorts.length) {
212
+ return validSorts.join(",");
213
+ }
214
+ }
215
+ return void 0;
216
+ }
217
+ /**
218
+ * Perform the actual create operation
219
+ */
220
+ async entityCreate(data) {
221
+ return await this.getCollection().create(data);
222
+ }
223
+ /**
224
+ * Perform the actual update operation
225
+ */
226
+ async entityUpdate(id, data) {
227
+ return await this.getCollection().update(id, data);
228
+ }
229
+ /**
230
+ * Perform the actual getById operation
231
+ */
232
+ async entityGetById(id, expand) {
233
+ const finalExpand = this.prepareExpand(expand);
234
+ const options = finalExpand ? { expand: finalExpand } : {};
235
+ return await this.getCollection().getOne(id, options);
236
+ }
237
+ /**
238
+ * Perform the actual getFirstByFilter operation
239
+ */
240
+ async entityGetFirstByFilter(filter, expand, sort) {
241
+ const finalFilter = this.prepareFilter(filter);
242
+ const finalExpand = this.prepareExpand(expand);
243
+ const finalSort = this.prepareSort(sort);
244
+ const options = {};
245
+ if (finalExpand) options.expand = finalExpand;
246
+ if (finalSort) options.sort = finalSort;
247
+ return await this.getCollection().getFirstListItem(finalFilter || "", options);
248
+ }
249
+ /**
250
+ * Perform the actual getList operation
251
+ * Returns a list result with items of type T
252
+ */
253
+ async entityGetList(page, perPage, filter, sort, expand) {
254
+ const finalFilter = this.prepareFilter(filter);
255
+ const finalExpand = this.prepareExpand(expand);
256
+ const finalSort = this.prepareSort(sort);
257
+ const options = {};
258
+ if (finalFilter) options.filter = finalFilter;
259
+ if (finalExpand) options.expand = finalExpand;
260
+ if (finalSort) options.sort = finalSort;
261
+ return await this.getCollection().getList(page, perPage, options);
262
+ }
263
+ /**
264
+ * Perform the actual delete operation
265
+ */
266
+ async entityDelete(id) {
267
+ await this.getCollection().delete(id);
268
+ return true;
269
+ }
270
+ /**
271
+ * Error handler for common errors
272
+ * @param error The error to handle
273
+ * @param options Handler options
274
+ * @returns The value to return if the error is handled, or throws if not handled
275
+ */
276
+ handleError(error, options = { logError: true }) {
277
+ const { allowNotFound = false, returnValue, logError: logError2 = true } = options;
278
+ if (logError2) {
279
+ console.error(`Error in ${this.constructor.name}:`, error);
280
+ }
281
+ if (allowNotFound && this.isNotFoundError(error)) {
282
+ return null;
283
+ }
284
+ if (returnValue !== void 0) {
285
+ return returnValue;
286
+ }
287
+ throw error;
288
+ }
289
+ /**
290
+ * Check if an error is a "not found" error
291
+ */
292
+ isNotFoundError(error) {
293
+ return error instanceof Error && (error.message.includes("404") || error.message.toLowerCase().includes("not found"));
294
+ }
295
+ /**
296
+ * Standard error handling wrapper (legacy method, consider using handleError instead)
297
+ */
298
+ errorWrapper(error) {
299
+ console.error(`Error in ${this.constructor.name}:`, error);
300
+ throw error;
301
+ }
302
+ /**
303
+ * Subscribe to changes on a specific record
304
+ * @param id The ID of the record to subscribe to
305
+ * @param callback Function to call when changes occur
306
+ * @param expand Optional expand parameters
307
+ * @returns Promise that resolves to an unsubscribe function
308
+ */
309
+ async subscribeToRecord(id, callback, expand) {
310
+ const finalExpand = this.prepareExpand(expand);
311
+ const options = finalExpand ? { expand: finalExpand } : {};
312
+ return this.getCollection().subscribe(id, callback, options);
313
+ }
314
+ /**
315
+ * Subscribe to changes on the entire collection
316
+ * @param callback Function to call when changes occur
317
+ * @param expand Optional expand parameters
318
+ * @returns Promise that resolves to an unsubscribe function
319
+ */
320
+ async subscribeToCollection(callback, expand) {
321
+ const finalExpand = this.prepareExpand(expand);
322
+ const options = finalExpand ? { expand: finalExpand } : {};
323
+ return this.getCollection().subscribe("*", callback, options);
324
+ }
325
+ /**
326
+ * Unsubscribe from a specific record's changes
327
+ * @param id The ID of the record to unsubscribe from
328
+ */
329
+ unsubscribeFromRecord(id) {
330
+ this.getCollection().unsubscribe(id);
331
+ }
332
+ /**
333
+ * Unsubscribe from collection-wide changes
334
+ */
335
+ unsubscribeFromCollection() {
336
+ this.getCollection().unsubscribe("*");
337
+ }
338
+ /**
339
+ * Unsubscribe from all subscriptions in this collection
340
+ */
341
+ unsubscribeAll() {
342
+ this.getCollection().unsubscribe();
343
+ }
344
+ };
18
345
  var baseSchema = {
19
346
  id: z.string().describe("unique id"),
20
347
  collectionId: z.string().describe("collection id"),
@@ -37,762 +364,544 @@ var inputImageFileSchema = {
37
364
  imageFiles: z.array(z.instanceof(File))
38
365
  };
39
366
  var omitImageFilesSchema = {
40
- imageFiles: true
41
- };
42
- function textField(options) {
43
- let schema = z.string();
44
- if (options?.min !== void 0) schema = schema.min(options.min);
45
- if (options?.max !== void 0) schema = schema.max(options.max);
46
- if (options?.pattern !== void 0) schema = schema.regex(options.pattern);
47
- return schema;
48
- }
49
- function emailField() {
50
- return z.string().email();
51
- }
52
- function urlField() {
53
- return z.string().url();
54
- }
55
- function numberField(options) {
56
- let schema = z.number();
57
- if (options?.min !== void 0) schema = schema.min(options.min);
58
- if (options?.max !== void 0) schema = schema.max(options.max);
59
- return schema;
60
- }
61
- function boolField() {
62
- return z.boolean();
63
- }
64
- function dateField() {
65
- return z.date();
66
- }
67
- function selectField(values) {
68
- return z.enum(values);
69
- }
70
- function jsonField(schema) {
71
- return schema ?? z.record(z.any());
72
- }
73
- function fileField() {
74
- return z.instanceof(File);
75
- }
76
- function filesField(options) {
77
- let schema = z.array(z.instanceof(File));
78
- if (options?.min !== void 0) schema = schema.min(options.min);
79
- if (options?.max !== void 0) schema = schema.max(options.max);
80
- return schema;
81
- }
82
- var RELATION_METADATA_KEY = "__pocketbase_relation__";
83
- function RelationField(config) {
84
- const metadata = {
85
- [RELATION_METADATA_KEY]: {
86
- type: "single",
87
- collection: config.collection,
88
- cascadeDelete: config.cascadeDelete ?? false,
89
- maxSelect: 1,
90
- minSelect: 0
91
- }
92
- };
93
- return z.string().describe(JSON.stringify(metadata));
94
- }
95
- function RelationsField(config) {
96
- const metadata = {
97
- [RELATION_METADATA_KEY]: {
98
- type: "multiple",
99
- collection: config.collection,
100
- cascadeDelete: config.cascadeDelete ?? false,
101
- maxSelect: config.maxSelect ?? 999,
102
- minSelect: config.minSelect ?? 0
103
- }
104
- };
105
- let schema = z.array(z.string());
106
- if (config.minSelect !== void 0) {
107
- schema = schema.min(config.minSelect);
108
- }
109
- if (config.maxSelect !== void 0) {
110
- schema = schema.max(config.maxSelect);
111
- }
112
- return schema.describe(JSON.stringify(metadata));
113
- }
114
- function extractRelationMetadata(description) {
115
- if (!description) return null;
116
- try {
117
- const parsed = JSON.parse(description);
118
- if (parsed[RELATION_METADATA_KEY]) {
119
- return parsed[RELATION_METADATA_KEY];
120
- }
121
- } catch {
122
- }
123
- return null;
124
- }
125
- function editorField() {
126
- return z.string();
127
- }
128
- function geoPointField() {
129
- return z.object({
130
- lon: z.number(),
131
- lat: z.number()
132
- });
133
- }
134
- function withPermissions(schema, config) {
135
- const metadata = {
136
- permissions: config
137
- };
138
- return schema.describe(JSON.stringify(metadata));
139
- }
140
- function withIndexes(schema, indexes) {
141
- let existingMetadata = {};
142
- if (schema.description) {
143
- try {
144
- existingMetadata = JSON.parse(schema.description);
145
- } catch {
146
- }
147
- }
148
- const metadata = {
149
- ...existingMetadata,
150
- indexes
151
- };
152
- return schema.describe(JSON.stringify(metadata));
153
- }
154
- function defineCollection(config) {
155
- const { collectionName, schema, permissions, indexes, type, ...futureOptions } = config;
156
- const metadata = {
157
- collectionName
158
- };
159
- if (type) {
160
- metadata.type = type;
161
- }
162
- if (permissions) {
163
- metadata.permissions = permissions;
164
- }
165
- if (indexes) {
166
- metadata.indexes = indexes;
167
- }
168
- if (Object.keys(futureOptions).length > 0) {
169
- Object.assign(metadata, futureOptions);
170
- }
171
- return schema.describe(JSON.stringify(metadata));
172
- }
173
-
174
- // src/utils/permission-templates.ts
175
- var PermissionTemplates = {
176
- /**
177
- * Public access - anyone can perform all operations
178
- */
179
- public: () => ({
180
- listRule: "",
181
- viewRule: "",
182
- createRule: "",
183
- updateRule: "",
184
- deleteRule: ""
185
- }),
186
- /**
187
- * Authenticated users only - requires valid authentication for all operations
188
- */
189
- authenticated: () => ({
190
- listRule: '@request.auth.id != ""',
191
- viewRule: '@request.auth.id != ""',
192
- createRule: '@request.auth.id != ""',
193
- updateRule: '@request.auth.id != ""',
194
- deleteRule: '@request.auth.id != ""'
195
- }),
196
- /**
197
- * Owner-only access - users can only manage their own records
198
- * @param ownerField - Name of the relation field pointing to user (default: 'User')
199
- */
200
- ownerOnly: (ownerField = "User") => ({
201
- listRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`,
202
- viewRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`,
203
- createRule: '@request.auth.id != ""',
204
- updateRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`,
205
- deleteRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`
206
- }),
207
- /**
208
- * Admin/superuser only access
209
- * Assumes a 'role' field exists with 'admin' value
210
- * @param roleField - Name of the role field (default: 'role')
211
- */
212
- adminOnly: (roleField = "role") => ({
213
- listRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
214
- viewRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
215
- createRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
216
- updateRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
217
- deleteRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`
218
- }),
219
- /**
220
- * Public read, authenticated write
221
- * Anyone can list/view, but only authenticated users can create/update/delete
222
- */
223
- readPublic: () => ({
224
- listRule: "",
225
- viewRule: "",
226
- createRule: '@request.auth.id != ""',
227
- updateRule: '@request.auth.id != ""',
228
- deleteRule: '@request.auth.id != ""'
229
- }),
230
- /**
231
- * Locked access - only superusers can perform operations
232
- * All rules are set to null (locked)
233
- */
234
- locked: () => ({
235
- listRule: null,
236
- viewRule: null,
237
- createRule: null,
238
- updateRule: null,
239
- deleteRule: null
240
- }),
241
- /**
242
- * Read-only authenticated - authenticated users can read, no write access
243
- */
244
- readOnlyAuthenticated: () => ({
245
- listRule: '@request.auth.id != ""',
246
- viewRule: '@request.auth.id != ""',
247
- createRule: null,
248
- updateRule: null,
249
- deleteRule: null
250
- })
251
- };
252
- function resolveTemplate(config) {
253
- let baseRules;
254
- switch (config.template) {
255
- case "public":
256
- baseRules = PermissionTemplates.public();
257
- break;
258
- case "authenticated":
259
- baseRules = PermissionTemplates.authenticated();
260
- break;
261
- case "owner-only":
262
- baseRules = PermissionTemplates.ownerOnly(config.ownerField);
263
- break;
264
- case "admin-only":
265
- baseRules = PermissionTemplates.adminOnly(config.roleField);
266
- break;
267
- case "read-public":
268
- baseRules = PermissionTemplates.readPublic();
269
- break;
270
- case "custom":
271
- baseRules = {};
272
- break;
273
- default: {
274
- const _exhaustive = config.template;
275
- throw new Error(`Unknown template type: ${_exhaustive}`);
276
- }
277
- }
278
- return {
279
- ...baseRules,
280
- ...config.customRules
281
- };
367
+ imageFiles: true
368
+ };
369
+ function textField(options) {
370
+ let schema = z.string();
371
+ if (options?.min !== void 0) schema = schema.min(options.min);
372
+ if (options?.max !== void 0) schema = schema.max(options.max);
373
+ if (options?.pattern !== void 0) schema = schema.regex(options.pattern);
374
+ return schema;
282
375
  }
283
- function isTemplateConfig(config) {
284
- return "template" in config;
376
+ function emailField() {
377
+ return z.string().email();
285
378
  }
286
- function isPermissionSchema(config) {
287
- return "listRule" in config || "viewRule" in config || "createRule" in config || "updateRule" in config || "deleteRule" in config || "manageRule" in config;
379
+ function urlField() {
380
+ return z.string().url();
288
381
  }
289
- function validatePermissionConfig(config, isAuthCollection2 = false) {
290
- const result = {
291
- valid: true,
292
- errors: [],
293
- warnings: []
294
- };
295
- let permissions;
296
- if (isTemplateConfig(config)) {
297
- if (config.template === "owner-only" && !config.ownerField) {
298
- result.warnings.push("owner-only template without ownerField specified - using default 'User'");
299
- }
300
- if (config.template === "admin-only" && !config.roleField) {
301
- result.warnings.push("admin-only template without roleField specified - using default 'role'");
302
- }
303
- permissions = resolveTemplate(config);
304
- } else {
305
- permissions = config;
306
- }
307
- if (permissions.manageRule !== void 0 && !isAuthCollection2) {
308
- result.errors.push("manageRule is only valid for auth collections");
309
- result.valid = false;
310
- }
311
- const ruleTypes = ["listRule", "viewRule", "createRule", "updateRule", "deleteRule"];
312
- if (isAuthCollection2) {
313
- ruleTypes.push("manageRule");
314
- }
315
- for (const ruleType of ruleTypes) {
316
- const rule = permissions[ruleType];
317
- if (rule !== void 0 && rule !== null && rule !== "") {
318
- const ruleValidation = validateRuleExpression(rule);
319
- if (!ruleValidation.valid) {
320
- result.errors.push(`${ruleType}: ${ruleValidation.errors.join(", ")}`);
321
- result.valid = false;
322
- }
323
- result.warnings.push(...ruleValidation.warnings.map((w) => `${ruleType}: ${w}`));
324
- }
325
- }
326
- return result;
382
+ function numberField(options) {
383
+ let schema = z.number();
384
+ if (options?.min !== void 0) schema = schema.min(options.min);
385
+ if (options?.max !== void 0) schema = schema.max(options.max);
386
+ return schema;
327
387
  }
328
- function validateRuleExpression(expression) {
329
- const result = {
330
- valid: true,
331
- errors: [],
332
- warnings: []
388
+ function boolField() {
389
+ return z.boolean();
390
+ }
391
+ function dateField() {
392
+ return z.date();
393
+ }
394
+ function selectField(values) {
395
+ return z.enum(values);
396
+ }
397
+ function jsonField(schema) {
398
+ return schema ?? z.record(z.any());
399
+ }
400
+ function fileField() {
401
+ return z.instanceof(File);
402
+ }
403
+ function filesField(options) {
404
+ let schema = z.array(z.instanceof(File));
405
+ if (options?.min !== void 0) schema = schema.min(options.min);
406
+ if (options?.max !== void 0) schema = schema.max(options.max);
407
+ return schema;
408
+ }
409
+ var RELATION_METADATA_KEY = "__pocketbase_relation__";
410
+ function RelationField(config) {
411
+ const metadata = {
412
+ [RELATION_METADATA_KEY]: {
413
+ type: "single",
414
+ collection: config.collection,
415
+ cascadeDelete: config.cascadeDelete ?? false,
416
+ maxSelect: 1,
417
+ minSelect: 0
418
+ }
333
419
  };
334
- if (expression === null || expression === "") {
335
- return result;
336
- }
337
- let parenCount = 0;
338
- for (const char of expression) {
339
- if (char === "(") parenCount++;
340
- if (char === ")") parenCount--;
341
- if (parenCount < 0) {
342
- result.errors.push("Unbalanced parentheses");
343
- result.valid = false;
344
- return result;
420
+ return z.string().describe(JSON.stringify(metadata));
421
+ }
422
+ function RelationsField(config) {
423
+ const metadata = {
424
+ [RELATION_METADATA_KEY]: {
425
+ type: "multiple",
426
+ collection: config.collection,
427
+ cascadeDelete: config.cascadeDelete ?? false,
428
+ maxSelect: config.maxSelect ?? 999,
429
+ minSelect: config.minSelect ?? 0
345
430
  }
431
+ };
432
+ let schema = z.array(z.string());
433
+ if (config.minSelect !== void 0) {
434
+ schema = schema.min(config.minSelect);
346
435
  }
347
- if (parenCount !== 0) {
348
- result.errors.push("Unbalanced parentheses");
349
- result.valid = false;
350
- }
351
- if (expression.includes("==")) {
352
- result.warnings.push("Use '=' instead of '==' for equality comparison");
436
+ if (config.maxSelect !== void 0) {
437
+ schema = schema.max(config.maxSelect);
353
438
  }
354
- const requestRefs = expression.match(/@request\.[a-zA-Z_][a-zA-Z0-9_.]*/g) || [];
355
- for (const ref of requestRefs) {
356
- const isValid = ref.startsWith("@request.auth.") || ref === "@request.method" || ref === "@request.context" || ref.startsWith("@request.body.") || ref.startsWith("@request.query.") || ref.startsWith("@request.headers.");
357
- if (!isValid) {
358
- result.errors.push(`Invalid @request reference: '${ref}'`);
359
- result.valid = false;
439
+ return schema.describe(JSON.stringify(metadata));
440
+ }
441
+ function extractRelationMetadata(description) {
442
+ if (!description) return null;
443
+ try {
444
+ const parsed = JSON.parse(description);
445
+ if (parsed[RELATION_METADATA_KEY]) {
446
+ return parsed[RELATION_METADATA_KEY];
360
447
  }
448
+ } catch {
361
449
  }
362
- return result;
450
+ return null;
363
451
  }
364
- function createPermissions(permissions) {
365
- return {
366
- listRule: permissions.listRule ?? null,
367
- viewRule: permissions.viewRule ?? null,
368
- createRule: permissions.createRule ?? null,
369
- updateRule: permissions.updateRule ?? null,
370
- deleteRule: permissions.deleteRule ?? null,
371
- manageRule: permissions.manageRule ?? null
372
- };
452
+ function editorField() {
453
+ return z.string();
373
454
  }
374
- function mergePermissions(...schemas) {
375
- const merged = {
376
- listRule: null,
377
- viewRule: null,
378
- createRule: null,
379
- updateRule: null,
380
- deleteRule: null,
381
- manageRule: null
455
+ function geoPointField() {
456
+ return z.object({
457
+ lon: z.number(),
458
+ lat: z.number()
459
+ });
460
+ }
461
+ function withPermissions(schema, config) {
462
+ const metadata = {
463
+ permissions: config
382
464
  };
383
- for (const schema of schemas) {
384
- if (schema.listRule !== void 0) merged.listRule = schema.listRule;
385
- if (schema.viewRule !== void 0) merged.viewRule = schema.viewRule;
386
- if (schema.createRule !== void 0) merged.createRule = schema.createRule;
387
- if (schema.updateRule !== void 0) merged.updateRule = schema.updateRule;
388
- if (schema.deleteRule !== void 0) merged.deleteRule = schema.deleteRule;
389
- if (schema.manageRule !== void 0) merged.manageRule = schema.manageRule;
390
- }
391
- return merged;
465
+ return schema.describe(JSON.stringify(metadata));
392
466
  }
393
- var ProjectInputSchema = z.object({
394
- // Required fields
395
- title: z.string(),
396
- content: z.string(),
397
- status: StatusEnum,
398
- summary: z.string().optional(),
399
- OwnerUser: RelationField({ collection: "Users" }),
400
- SubscriberUsers: RelationsField({ collection: "Users" })
401
- }).extend(inputImageFileSchema);
402
- var ProjectSchema = ProjectInputSchema.omit(omitImageFilesSchema).extend(baseImageFileSchema);
403
- var ProjectCollection = defineCollection({
404
- collectionName: "Projects",
405
- schema: ProjectSchema,
406
- permissions: {
407
- template: "owner-only",
408
- ownerField: "OwnerUser",
409
- customRules: {
410
- listRule: '@request.auth.id != ""',
411
- viewRule: '@request.auth.id != "" && (OwnerUser = @request.auth.id || SubscriberUsers ?= @request.auth.id)'
467
+ function withIndexes(schema, indexes) {
468
+ let existingMetadata = {};
469
+ if (schema.description) {
470
+ try {
471
+ existingMetadata = JSON.parse(schema.description);
472
+ } catch {
412
473
  }
413
474
  }
414
- });
415
- var UserInputSchema = z.object({
416
- name: z.string().optional(),
417
- email: z.string().email(),
418
- password: z.string().min(8, "Password must be at least 8 characters"),
419
- passwordConfirm: z.string(),
420
- avatar: z.instanceof(File).optional()
421
- });
422
- var UserCollectionSchema = z.object({
423
- name: z.string().optional(),
424
- email: z.string().email(),
425
- password: z.string().min(8, "Password must be at least 8 characters"),
426
- avatar: z.instanceof(File).optional()
427
- });
428
- var UserSchema = UserCollectionSchema.extend(baseSchema);
429
- var UserCollection = defineCollection({
430
- collectionName: "users",
431
- type: "auth",
432
- schema: UserSchema,
433
- permissions: {
434
- // Users can list their own profile
435
- listRule: "id = @request.auth.id",
436
- // Users can view their own profile
437
- viewRule: "id = @request.auth.id",
438
- // Anyone can create an account (sign up)
439
- createRule: "",
440
- // Users can only update their own profile
441
- updateRule: "id = @request.auth.id",
442
- // Users can only delete their own account
443
- deleteRule: "id = @request.auth.id"
444
- // manageRule is null in PocketBase default (not set)
445
- },
446
- indexes: [
447
- // PocketBase's default indexes for auth collections
448
- "CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)",
449
- "CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''"
450
- ]
451
- });
452
- var BaseMutator = class {
453
- pb;
454
- // Define a default property that subclasses will override
455
- options = {
456
- expand: [],
457
- filter: [],
458
- sort: []
475
+ const metadata = {
476
+ ...existingMetadata,
477
+ indexes
459
478
  };
460
- constructor(pb, options) {
461
- this.pb = pb;
462
- this.initializeOptions();
463
- if (options) {
464
- this.overrideOptions(options);
465
- }
479
+ return schema.describe(JSON.stringify(metadata));
480
+ }
481
+ function defineCollection(config) {
482
+ const { collectionName, schema, permissions, indexes, type, ...futureOptions } = config;
483
+ const metadata = {
484
+ collectionName
485
+ };
486
+ if (type) {
487
+ metadata.type = type;
466
488
  }
467
- initializeOptions() {
468
- this.options = this.setDefaults();
489
+ if (permissions) {
490
+ metadata.permissions = permissions;
469
491
  }
470
- /**
471
- * Initialize options with class-specific defaults
472
- * Subclasses should override this instead of directly setting options
473
- */
474
- setDefaults() {
475
- return {
476
- expand: [],
477
- filter: [],
478
- sort: []
479
- };
492
+ if (indexes) {
493
+ metadata.indexes = indexes;
480
494
  }
481
- /**
482
- * Merge provided options with current options
483
- */
484
- overrideOptions(newOptions) {
485
- if (newOptions.expand !== void 0) {
486
- this.options.expand = newOptions.expand;
487
- }
488
- if (newOptions.filter !== void 0) {
489
- this.options.filter = newOptions.filter;
490
- }
491
- if (newOptions.sort !== void 0) {
492
- this.options.sort = newOptions.sort;
495
+ if (Object.keys(futureOptions).length > 0) {
496
+ Object.assign(metadata, futureOptions);
497
+ }
498
+ return schema.describe(JSON.stringify(metadata));
499
+ }
500
+ var FIELD_METADATA_KEY = "__pocketbase_field__";
501
+ function extractFieldMetadata(description) {
502
+ if (!description) return null;
503
+ try {
504
+ const parsed = JSON.parse(description);
505
+ if (parsed[FIELD_METADATA_KEY]) {
506
+ return parsed[FIELD_METADATA_KEY];
493
507
  }
508
+ } catch {
494
509
  }
495
- toSnakeCase(str) {
496
- return str.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
497
- }
498
- /**
499
- * Create a new entity
500
- */
501
- async create(input) {
502
- try {
503
- const data = await this.validateInput(input);
504
- const record = await this.entityCreate(data);
505
- return await this.processRecord(record);
506
- } catch (error) {
507
- return this.errorWrapper(error);
510
+ return null;
511
+ }
512
+ function BoolField() {
513
+ const metadata = {
514
+ [FIELD_METADATA_KEY]: {
515
+ type: "bool"
508
516
  }
509
- }
510
- /**
511
- * Update an existing entity
512
- */
513
- async update(id, input) {
514
- try {
515
- const record = await this.entityUpdate(id, input);
516
- return await this.processRecord(record);
517
- } catch (error) {
518
- return this.errorWrapper(error);
517
+ };
518
+ return z.boolean().describe(JSON.stringify(metadata));
519
+ }
520
+ function NumberField(options) {
521
+ if (options?.min !== void 0 && options?.max !== void 0) {
522
+ if (options.min > options.max) {
523
+ throw new Error("NumberField: min cannot be greater than max");
519
524
  }
520
525
  }
521
- /**
522
- * Create or update entity (upsert)
523
- */
524
- async upsert(input) {
525
- if (input?.id) {
526
- return await this.update(input.id, input);
527
- }
528
- return await this.create(input);
526
+ let schema = z.number();
527
+ if (options?.min !== void 0) {
528
+ schema = schema.min(options.min);
529
529
  }
530
- /**
531
- * Get entity by ID
532
- */
533
- async getById(id, expand) {
534
- try {
535
- const record = await this.entityGetById(id, expand);
536
- return await this.processRecord(record);
537
- } catch (error) {
538
- return this.handleError(error, { allowNotFound: true });
539
- }
530
+ if (options?.max !== void 0) {
531
+ schema = schema.max(options.max);
540
532
  }
541
- /**
542
- * Get first entity by filter
543
- */
544
- async getFirstByFilter(filter, expand, sort) {
545
- try {
546
- const record = await this.entityGetFirstByFilter(filter, expand, sort);
547
- return await this.processRecord(record);
548
- } catch (error) {
549
- return this.handleError(error, { allowNotFound: true });
533
+ const metadata = {
534
+ [FIELD_METADATA_KEY]: {
535
+ type: "number",
536
+ options: options || {}
550
537
  }
551
- }
552
- /**
553
- * Get list of entities
554
- */
555
- async getList(page = 1, perPage = 100, filter, sort, expand) {
556
- try {
557
- const result = await this.entityGetList(page, perPage, filter, sort, expand);
558
- return await this.processListResult(result);
559
- } catch (error) {
560
- return this.errorWrapper(error);
538
+ };
539
+ return schema.describe(JSON.stringify(metadata));
540
+ }
541
+ function TextField(options) {
542
+ if (options?.min !== void 0 && options?.max !== void 0) {
543
+ if (options.min > options.max) {
544
+ throw new Error("TextField: min cannot be greater than max");
561
545
  }
562
546
  }
563
- /**
564
- * Delete entity by ID
565
- */
566
- async delete(id) {
567
- try {
568
- return await this.entityDelete(id);
569
- } catch (error) {
570
- return this.handleError(error, { returnValue: false });
571
- }
547
+ let schema = z.string();
548
+ if (options?.min !== void 0) {
549
+ schema = schema.min(options.min);
572
550
  }
573
- /**
574
- * Process a single record before returning it
575
- * Can be overridden to handle special cases like mapped entities
576
- */
577
- async processRecord(record) {
578
- return record;
551
+ if (options?.max !== void 0) {
552
+ schema = schema.max(options.max);
579
553
  }
580
- /**
581
- * Process a list result before returning it
582
- * Can be overridden to handle special cases like mapped entities
583
- */
584
- async processListResult(result) {
585
- const processedItems = await Promise.all(result.items.map((item) => this.processRecord(item)));
586
- return {
587
- ...result,
588
- items: processedItems
589
- };
554
+ if (options?.pattern !== void 0) {
555
+ const pattern = options.pattern instanceof RegExp ? options.pattern : new RegExp(options.pattern);
556
+ schema = schema.regex(pattern);
590
557
  }
591
- /**
592
- * Prepare expand parameter
593
- * Combines default expands with provided expands
594
- */
595
- prepareExpand(expand) {
596
- if (!this.options.expand.length && !expand) {
597
- return void 0;
558
+ const metadata = {
559
+ [FIELD_METADATA_KEY]: {
560
+ type: "text",
561
+ options: options || {}
598
562
  }
599
- let expandArray = [...this.options.expand];
600
- if (expand) {
601
- if (typeof expand === "string") {
602
- expandArray = expandArray.concat(expand.split(",").map((e) => e.trim()));
603
- } else {
604
- expandArray = expandArray.concat(expand);
605
- }
563
+ };
564
+ return schema.describe(JSON.stringify(metadata));
565
+ }
566
+ function EmailField() {
567
+ const metadata = {
568
+ [FIELD_METADATA_KEY]: {
569
+ type: "email"
606
570
  }
607
- const uniqueExpands = [...new Set(expandArray)].filter((e) => e !== "" && e !== void 0);
608
- if (!uniqueExpands.length) {
609
- return void 0;
571
+ };
572
+ return z.string().email().describe(JSON.stringify(metadata));
573
+ }
574
+ function URLField() {
575
+ const metadata = {
576
+ [FIELD_METADATA_KEY]: {
577
+ type: "url"
578
+ }
579
+ };
580
+ return z.string().url().describe(JSON.stringify(metadata));
581
+ }
582
+ function EditorField() {
583
+ const metadata = {
584
+ [FIELD_METADATA_KEY]: {
585
+ type: "editor"
586
+ }
587
+ };
588
+ return z.string().describe(JSON.stringify(metadata));
589
+ }
590
+ function DateField(options) {
591
+ if (options?.min !== void 0 && options?.max !== void 0) {
592
+ const minDate = typeof options.min === "string" ? new Date(options.min) : options.min;
593
+ const maxDate = typeof options.max === "string" ? new Date(options.max) : options.max;
594
+ if (minDate > maxDate) {
595
+ throw new Error("DateField: min cannot be greater than max");
610
596
  }
611
- return uniqueExpands.join(",");
612
597
  }
613
- /**
614
- * Prepare filter parameter
615
- * Combines default filters with provided filters
616
- */
617
- prepareFilter(filter) {
618
- if (!this.options.filter.length && !filter) {
619
- return void 0;
598
+ const schema = z.string();
599
+ const metadata = {
600
+ [FIELD_METADATA_KEY]: {
601
+ type: "date",
602
+ options: options || {}
620
603
  }
621
- let filterArray = [...this.options.filter];
622
- if (filter) {
623
- if (typeof filter === "string") {
624
- if (filter) filterArray.push(filter);
625
- } else {
626
- filterArray = filterArray.concat(filter);
627
- }
604
+ };
605
+ return schema.describe(JSON.stringify(metadata));
606
+ }
607
+ function AutodateField(options) {
608
+ const schema = z.string();
609
+ const metadata = {
610
+ [FIELD_METADATA_KEY]: {
611
+ type: "autodate",
612
+ options: options || {}
628
613
  }
629
- const validFilters = filterArray.filter((f) => f !== "" && f !== void 0);
630
- if (!validFilters.length) {
631
- return void 0;
614
+ };
615
+ return schema.describe(JSON.stringify(metadata));
616
+ }
617
+ function SelectField(values, options) {
618
+ const enumSchema = z.enum(values);
619
+ const metadata = {
620
+ [FIELD_METADATA_KEY]: {
621
+ type: "select",
622
+ options: {
623
+ values,
624
+ maxSelect: options?.maxSelect ?? 1
625
+ }
632
626
  }
633
- return validFilters.join("&&");
627
+ };
628
+ if (options?.maxSelect && options.maxSelect > 1) {
629
+ return z.array(enumSchema).describe(JSON.stringify(metadata));
634
630
  }
635
- /**
636
- * Prepare sort parameter
637
- * Uses provided sort or falls back to default sort
638
- */
639
- prepareSort(sort) {
640
- if (sort && sort !== "") {
641
- return sort;
631
+ return enumSchema.describe(JSON.stringify(metadata));
632
+ }
633
+ function FileField(options) {
634
+ const schema = z.instanceof(File);
635
+ const metadata = {
636
+ [FIELD_METADATA_KEY]: {
637
+ type: "file",
638
+ options: options || {}
642
639
  }
643
- if (this.options.sort.length) {
644
- const validSorts = this.options.sort.filter((s) => s !== "" && s !== void 0);
645
- if (validSorts.length) {
646
- return validSorts.join(",");
647
- }
640
+ };
641
+ return schema.describe(JSON.stringify(metadata));
642
+ }
643
+ function FilesField(options) {
644
+ if (options?.minSelect !== void 0 && options?.maxSelect !== void 0) {
645
+ if (options.minSelect > options.maxSelect) {
646
+ throw new Error("FilesField: minSelect cannot be greater than maxSelect");
648
647
  }
649
- return void 0;
650
648
  }
651
- /**
652
- * Perform the actual create operation
653
- */
654
- async entityCreate(data) {
655
- return await this.getCollection().create(data);
649
+ let schema = z.array(z.instanceof(File));
650
+ if (options?.minSelect !== void 0) {
651
+ schema = schema.min(options.minSelect);
652
+ }
653
+ if (options?.maxSelect !== void 0) {
654
+ schema = schema.max(options.maxSelect);
656
655
  }
656
+ const metadata = {
657
+ [FIELD_METADATA_KEY]: {
658
+ type: "file",
659
+ options: options || {}
660
+ }
661
+ };
662
+ return schema.describe(JSON.stringify(metadata));
663
+ }
664
+ function JSONField(schema) {
665
+ const baseSchema2 = schema ?? z.record(z.string(), z.any());
666
+ const metadata = {
667
+ [FIELD_METADATA_KEY]: {
668
+ type: "json"
669
+ }
670
+ };
671
+ return baseSchema2.describe(JSON.stringify(metadata));
672
+ }
673
+ function GeoPointField() {
674
+ const schema = z.object({
675
+ lon: z.number(),
676
+ lat: z.number()
677
+ });
678
+ const metadata = {
679
+ [FIELD_METADATA_KEY]: {
680
+ type: "geoPoint"
681
+ }
682
+ };
683
+ return schema.describe(JSON.stringify(metadata));
684
+ }
685
+
686
+ // src/utils/permission-templates.ts
687
+ var PermissionTemplates = {
657
688
  /**
658
- * Perform the actual update operation
689
+ * Public access - anyone can perform all operations
659
690
  */
660
- async entityUpdate(id, data) {
661
- return await this.getCollection().update(id, data);
662
- }
691
+ public: () => ({
692
+ listRule: "",
693
+ viewRule: "",
694
+ createRule: "",
695
+ updateRule: "",
696
+ deleteRule: ""
697
+ }),
663
698
  /**
664
- * Perform the actual getById operation
699
+ * Authenticated users only - requires valid authentication for all operations
665
700
  */
666
- async entityGetById(id, expand) {
667
- const finalExpand = this.prepareExpand(expand);
668
- const options = finalExpand ? { expand: finalExpand } : {};
669
- return await this.getCollection().getOne(id, options);
670
- }
701
+ authenticated: () => ({
702
+ listRule: '@request.auth.id != ""',
703
+ viewRule: '@request.auth.id != ""',
704
+ createRule: '@request.auth.id != ""',
705
+ updateRule: '@request.auth.id != ""',
706
+ deleteRule: '@request.auth.id != ""'
707
+ }),
671
708
  /**
672
- * Perform the actual getFirstByFilter operation
709
+ * Owner-only access - users can only manage their own records
710
+ * @param ownerField - Name of the relation field pointing to user (default: 'User')
673
711
  */
674
- async entityGetFirstByFilter(filter, expand, sort) {
675
- const finalFilter = this.prepareFilter(filter);
676
- const finalExpand = this.prepareExpand(expand);
677
- const finalSort = this.prepareSort(sort);
678
- const options = {};
679
- if (finalExpand) options.expand = finalExpand;
680
- if (finalSort) options.sort = finalSort;
681
- return await this.getCollection().getFirstListItem(finalFilter || "", options);
682
- }
712
+ ownerOnly: (ownerField = "User") => ({
713
+ listRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`,
714
+ viewRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`,
715
+ createRule: '@request.auth.id != ""',
716
+ updateRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`,
717
+ deleteRule: `@request.auth.id != "" && ${ownerField} = @request.auth.id`
718
+ }),
719
+ /**
720
+ * Admin/superuser only access
721
+ * Assumes a 'role' field exists with 'admin' value
722
+ * @param roleField - Name of the role field (default: 'role')
723
+ */
724
+ adminOnly: (roleField = "role") => ({
725
+ listRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
726
+ viewRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
727
+ createRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
728
+ updateRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`,
729
+ deleteRule: `@request.auth.id != "" && @request.auth.${roleField} = "admin"`
730
+ }),
683
731
  /**
684
- * Perform the actual getList operation
685
- * Returns a list result with items of type T
732
+ * Public read, authenticated write
733
+ * Anyone can list/view, but only authenticated users can create/update/delete
686
734
  */
687
- async entityGetList(page, perPage, filter, sort, expand) {
688
- const finalFilter = this.prepareFilter(filter);
689
- const finalExpand = this.prepareExpand(expand);
690
- const finalSort = this.prepareSort(sort);
691
- const options = {};
692
- if (finalFilter) options.filter = finalFilter;
693
- if (finalExpand) options.expand = finalExpand;
694
- if (finalSort) options.sort = finalSort;
695
- return await this.getCollection().getList(page, perPage, options);
696
- }
735
+ readPublic: () => ({
736
+ listRule: "",
737
+ viewRule: "",
738
+ createRule: '@request.auth.id != ""',
739
+ updateRule: '@request.auth.id != ""',
740
+ deleteRule: '@request.auth.id != ""'
741
+ }),
697
742
  /**
698
- * Perform the actual delete operation
743
+ * Locked access - only superusers can perform operations
744
+ * All rules are set to null (locked)
699
745
  */
700
- async entityDelete(id) {
701
- await this.getCollection().delete(id);
702
- return true;
703
- }
746
+ locked: () => ({
747
+ listRule: null,
748
+ viewRule: null,
749
+ createRule: null,
750
+ updateRule: null,
751
+ deleteRule: null
752
+ }),
704
753
  /**
705
- * Error handler for common errors
706
- * @param error The error to handle
707
- * @param options Handler options
708
- * @returns The value to return if the error is handled, or throws if not handled
754
+ * Read-only authenticated - authenticated users can read, no write access
709
755
  */
710
- handleError(error, options = { logError: true }) {
711
- const { allowNotFound = false, returnValue, logError: logError2 = true } = options;
712
- if (logError2) {
713
- console.error(`Error in ${this.constructor.name}:`, error);
756
+ readOnlyAuthenticated: () => ({
757
+ listRule: '@request.auth.id != ""',
758
+ viewRule: '@request.auth.id != ""',
759
+ createRule: null,
760
+ updateRule: null,
761
+ deleteRule: null
762
+ })
763
+ };
764
+ function resolveTemplate(config) {
765
+ let baseRules;
766
+ switch (config.template) {
767
+ case "public":
768
+ baseRules = PermissionTemplates.public();
769
+ break;
770
+ case "authenticated":
771
+ baseRules = PermissionTemplates.authenticated();
772
+ break;
773
+ case "owner-only":
774
+ baseRules = PermissionTemplates.ownerOnly(config.ownerField);
775
+ break;
776
+ case "admin-only":
777
+ baseRules = PermissionTemplates.adminOnly(config.roleField);
778
+ break;
779
+ case "read-public":
780
+ baseRules = PermissionTemplates.readPublic();
781
+ break;
782
+ case "custom":
783
+ baseRules = {};
784
+ break;
785
+ default: {
786
+ const _exhaustive = config.template;
787
+ throw new Error(`Unknown template type: ${_exhaustive}`);
714
788
  }
715
- if (allowNotFound && this.isNotFoundError(error)) {
716
- return null;
789
+ }
790
+ return {
791
+ ...baseRules,
792
+ ...config.customRules
793
+ };
794
+ }
795
+ function isTemplateConfig(config) {
796
+ return "template" in config;
797
+ }
798
+ function isPermissionSchema(config) {
799
+ return "listRule" in config || "viewRule" in config || "createRule" in config || "updateRule" in config || "deleteRule" in config || "manageRule" in config;
800
+ }
801
+ function validatePermissionConfig(config, isAuthCollection2 = false) {
802
+ const result = {
803
+ valid: true,
804
+ errors: [],
805
+ warnings: []
806
+ };
807
+ let permissions;
808
+ if (isTemplateConfig(config)) {
809
+ if (config.template === "owner-only" && !config.ownerField) {
810
+ result.warnings.push("owner-only template without ownerField specified - using default 'User'");
717
811
  }
718
- if (returnValue !== void 0) {
719
- return returnValue;
812
+ if (config.template === "admin-only" && !config.roleField) {
813
+ result.warnings.push("admin-only template without roleField specified - using default 'role'");
720
814
  }
721
- throw error;
722
- }
723
- /**
724
- * Check if an error is a "not found" error
725
- */
726
- isNotFoundError(error) {
727
- return error instanceof Error && (error.message.includes("404") || error.message.toLowerCase().includes("not found"));
815
+ permissions = resolveTemplate(config);
816
+ } else {
817
+ permissions = config;
728
818
  }
729
- /**
730
- * Standard error handling wrapper (legacy method, consider using handleError instead)
731
- */
732
- errorWrapper(error) {
733
- console.error(`Error in ${this.constructor.name}:`, error);
734
- throw error;
819
+ if (permissions.manageRule !== void 0 && !isAuthCollection2) {
820
+ result.errors.push("manageRule is only valid for auth collections");
821
+ result.valid = false;
735
822
  }
736
- /**
737
- * Subscribe to changes on a specific record
738
- * @param id The ID of the record to subscribe to
739
- * @param callback Function to call when changes occur
740
- * @param expand Optional expand parameters
741
- * @returns Promise that resolves to an unsubscribe function
742
- */
743
- async subscribeToRecord(id, callback, expand) {
744
- const finalExpand = this.prepareExpand(expand);
745
- const options = finalExpand ? { expand: finalExpand } : {};
746
- return this.getCollection().subscribe(id, callback, options);
823
+ const ruleTypes = ["listRule", "viewRule", "createRule", "updateRule", "deleteRule"];
824
+ if (isAuthCollection2) {
825
+ ruleTypes.push("manageRule");
747
826
  }
748
- /**
749
- * Subscribe to changes on the entire collection
750
- * @param callback Function to call when changes occur
751
- * @param expand Optional expand parameters
752
- * @returns Promise that resolves to an unsubscribe function
753
- */
754
- async subscribeToCollection(callback, expand) {
755
- const finalExpand = this.prepareExpand(expand);
756
- const options = finalExpand ? { expand: finalExpand } : {};
757
- return this.getCollection().subscribe("*", callback, options);
827
+ for (const ruleType of ruleTypes) {
828
+ const rule = permissions[ruleType];
829
+ if (rule !== void 0 && rule !== null && rule !== "") {
830
+ const ruleValidation = validateRuleExpression(rule);
831
+ if (!ruleValidation.valid) {
832
+ result.errors.push(`${ruleType}: ${ruleValidation.errors.join(", ")}`);
833
+ result.valid = false;
834
+ }
835
+ result.warnings.push(...ruleValidation.warnings.map((w) => `${ruleType}: ${w}`));
836
+ }
758
837
  }
759
- /**
760
- * Unsubscribe from a specific record's changes
761
- * @param id The ID of the record to unsubscribe from
762
- */
763
- unsubscribeFromRecord(id) {
764
- this.getCollection().unsubscribe(id);
838
+ return result;
839
+ }
840
+ function validateRuleExpression(expression) {
841
+ const result = {
842
+ valid: true,
843
+ errors: [],
844
+ warnings: []
845
+ };
846
+ if (expression === null || expression === "") {
847
+ return result;
765
848
  }
766
- /**
767
- * Unsubscribe from collection-wide changes
768
- */
769
- unsubscribeFromCollection() {
770
- this.getCollection().unsubscribe("*");
849
+ let parenCount = 0;
850
+ for (const char of expression) {
851
+ if (char === "(") parenCount++;
852
+ if (char === ")") parenCount--;
853
+ if (parenCount < 0) {
854
+ result.errors.push("Unbalanced parentheses");
855
+ result.valid = false;
856
+ return result;
857
+ }
771
858
  }
772
- /**
773
- * Unsubscribe from all subscriptions in this collection
774
- */
775
- unsubscribeAll() {
776
- this.getCollection().unsubscribe();
859
+ if (parenCount !== 0) {
860
+ result.errors.push("Unbalanced parentheses");
861
+ result.valid = false;
777
862
  }
778
- };
779
-
780
- // src/mutator/userMutator.ts
781
- var UserMutator = class extends BaseMutator {
782
- setDefaults() {
783
- return {
784
- expand: [],
785
- filter: [],
786
- sort: ["-updated"]
787
- };
863
+ if (expression.includes("==")) {
864
+ result.warnings.push("Use '=' instead of '==' for equality comparison");
788
865
  }
789
- getCollection() {
790
- return this.pb.collection("Users");
866
+ const requestRefs = expression.match(/@request\.[a-zA-Z_][a-zA-Z0-9_.]*/g) || [];
867
+ for (const ref of requestRefs) {
868
+ const isValid = ref.startsWith("@request.auth.") || ref === "@request.method" || ref === "@request.context" || ref.startsWith("@request.body.") || ref.startsWith("@request.query.") || ref.startsWith("@request.headers.");
869
+ if (!isValid) {
870
+ result.errors.push(`Invalid @request reference: '${ref}'`);
871
+ result.valid = false;
872
+ }
791
873
  }
792
- async validateInput(input) {
793
- return UserInputSchema.parse(input);
874
+ return result;
875
+ }
876
+ function createPermissions(permissions) {
877
+ return {
878
+ listRule: permissions.listRule ?? null,
879
+ viewRule: permissions.viewRule ?? null,
880
+ createRule: permissions.createRule ?? null,
881
+ updateRule: permissions.updateRule ?? null,
882
+ deleteRule: permissions.deleteRule ?? null,
883
+ manageRule: permissions.manageRule ?? null
884
+ };
885
+ }
886
+ function mergePermissions(...schemas) {
887
+ const merged = {
888
+ listRule: null,
889
+ viewRule: null,
890
+ createRule: null,
891
+ updateRule: null,
892
+ deleteRule: null,
893
+ manageRule: null
894
+ };
895
+ for (const schema of schemas) {
896
+ if (schema.listRule !== void 0) merged.listRule = schema.listRule;
897
+ if (schema.viewRule !== void 0) merged.viewRule = schema.viewRule;
898
+ if (schema.createRule !== void 0) merged.createRule = schema.createRule;
899
+ if (schema.updateRule !== void 0) merged.updateRule = schema.updateRule;
900
+ if (schema.deleteRule !== void 0) merged.deleteRule = schema.deleteRule;
901
+ if (schema.manageRule !== void 0) merged.manageRule = schema.manageRule;
794
902
  }
795
- };
903
+ return merged;
904
+ }
796
905
 
797
906
  // src/migration/errors.ts
798
907
  var MigrationError = class _MigrationError extends Error {
@@ -2193,6 +2302,28 @@ function isAuthCollection(fields) {
2193
2302
  return hasEmail && hasPassword;
2194
2303
  }
2195
2304
  function buildFieldDefinition(fieldName, zodType) {
2305
+ const fieldMetadata = extractFieldMetadata(zodType.description);
2306
+ if (fieldMetadata) {
2307
+ const required2 = isFieldRequired(zodType);
2308
+ const fieldDef2 = {
2309
+ name: fieldName,
2310
+ type: fieldMetadata.type,
2311
+ required: required2,
2312
+ options: fieldMetadata.options
2313
+ };
2314
+ if (fieldMetadata.type === "relation") {
2315
+ const relationMetadata2 = extractRelationMetadata(zodType.description);
2316
+ if (relationMetadata2) {
2317
+ fieldDef2.relation = {
2318
+ collection: relationMetadata2.collection,
2319
+ maxSelect: relationMetadata2.maxSelect,
2320
+ minSelect: relationMetadata2.minSelect,
2321
+ cascadeDelete: relationMetadata2.cascadeDelete
2322
+ };
2323
+ }
2324
+ }
2325
+ return fieldDef2;
2326
+ }
2196
2327
  const fieldType = mapZodTypeToPocketBase(zodType, fieldName);
2197
2328
  const required = isFieldRequired(zodType);
2198
2329
  const options = extractFieldOptions(zodType);
@@ -5319,6 +5450,6 @@ async function executeStatus(options) {
5319
5450
  }
5320
5451
  }
5321
5452
 
5322
- export { CLIUsageError, ConfigurationError, DiffEngine, FIELD_TYPE_INFO, FileSystemError, MigrationError, MigrationGenerationError, MigrationGenerator, POCKETBASE_FIELD_TYPES, PermissionTemplates, ProjectCollection, ProjectInputSchema, ProjectSchema, RelationField, RelationsField, SchemaAnalyzer, SchemaParsingError, SnapshotError, SnapshotManager, StatusEnum, UserCollection, UserCollectionSchema, UserInputSchema, UserMutator, UserSchema, aggregateChanges, baseImageFileSchema, baseSchema, baseSchemaWithTimestamps, boolField, buildFieldDefinition, buildSchemaDefinition, categorizeChangesBySeverity, compare, compareFieldConstraints, compareFieldOptions, compareFieldTypes, comparePermissions, compareRelationConfigurations, convertPocketBaseMigration, convertZodSchemaToCollectionSchema, createMigrationFileStructure, createPermissions, dateField, defineCollection, detectDestructiveChanges, detectFieldChanges, discoverSchemaFiles, editorField, emailField, extractComprehensiveFieldOptions, extractFieldDefinitions, extractFieldOptions, extractIndexes, extractRelationMetadata, extractSchemaDefinitions, fileField, filesField, filterSystemCollections, findLatestSnapshot, findNewCollections, findNewFields, findRemovedCollections, findRemovedFields, formatChangeSummary, generate, generateChangeSummary, generateCollectionCreation, generateCollectionPermissions, generateCollectionRules, generateDownMigration, generateFieldAddition, generateFieldDefinitionObject, generateFieldDeletion, generateFieldModification, generateFieldsArray, generateIndexesArray, executeGenerate as generateMigration, generateMigrationDescription, generateMigrationFilename, generatePermissionUpdate, generateTimestamp, generateUpMigration, geoPointField, getArrayElementType, getCollectionNameFromFile, getDefaultValue, getFieldTypeInfo, getMaxSelect, executeStatus as getMigrationStatus, getMinSelect, getSnapshotPath, getSnapshotVersion, getUsersSystemFields, importSchemaModule, inputImageFileSchema, isArrayType, isAuthCollection, isEditorField, isFieldRequired, isFileFieldByName, isGeoPointType, isMultipleRelationField, isPermissionSchema, isRelationField, isSingleRelationField, isSystemCollection, isTemplateConfig, jsonField, loadBaseMigration, loadConfig, loadSnapshot, loadSnapshotIfExists, loadSnapshotWithMigrations, logError, logInfo, logSection, logSuccess, logWarning, mapZodArrayType, mapZodBooleanType, mapZodDateType, mapZodEnumType, mapZodNumberType, mapZodRecordType, mapZodStringType, mapZodTypeToPocketBase, matchCollectionsByName, matchFieldsByName, mergePermissions, mergeSnapshots, numberField, omitImageFilesSchema, parseSchemaFiles, pluralize, requiresForceFlag, resolveTargetCollection, resolveTemplate, saveSnapshot, selectField, selectSchemaForCollection, singularize, snapshotExists, textField, toCollectionName, unwrapZodType, urlField, validatePermissionConfig, validateRuleExpression, validateSnapshot, withIndexes, withPermissions, withProgress, writeMigrationFile };
5453
+ export { AutodateField, BaseMutator, BoolField, CLIUsageError, ConfigurationError, DateField, DiffEngine, EditorField, EmailField, FIELD_METADATA_KEY, FIELD_TYPE_INFO, FileField, FileSystemError, FilesField, GeoPointField, JSONField, MigrationError, MigrationGenerationError, MigrationGenerator, NumberField, POCKETBASE_FIELD_TYPES, PermissionTemplates, RelationField, RelationsField, SchemaAnalyzer, SchemaParsingError, SelectField, SnapshotError, SnapshotManager, StatusEnum, TextField, URLField, aggregateChanges, baseImageFileSchema, baseSchema, baseSchemaWithTimestamps, boolField, buildFieldDefinition, buildSchemaDefinition, categorizeChangesBySeverity, compare, compareFieldConstraints, compareFieldOptions, compareFieldTypes, comparePermissions, compareRelationConfigurations, convertPocketBaseMigration, convertZodSchemaToCollectionSchema, createMigrationFileStructure, createPermissions, dateField, defineCollection, detectDestructiveChanges, detectFieldChanges, discoverSchemaFiles, editorField, emailField, extractComprehensiveFieldOptions, extractFieldDefinitions, extractFieldMetadata, extractFieldOptions, extractIndexes, extractRelationMetadata, extractSchemaDefinitions, fileField, filesField, filterSystemCollections, findLatestSnapshot, findNewCollections, findNewFields, findRemovedCollections, findRemovedFields, formatChangeSummary, generate, generateChangeSummary, generateCollectionCreation, generateCollectionPermissions, generateCollectionRules, generateDownMigration, generateFieldAddition, generateFieldDefinitionObject, generateFieldDeletion, generateFieldModification, generateFieldsArray, generateIndexesArray, executeGenerate as generateMigration, generateMigrationDescription, generateMigrationFilename, generatePermissionUpdate, generateTimestamp, generateUpMigration, geoPointField, getArrayElementType, getCollectionNameFromFile, getDefaultValue, getFieldTypeInfo, getMaxSelect, executeStatus as getMigrationStatus, getMinSelect, getSnapshotPath, getSnapshotVersion, getUsersSystemFields, importSchemaModule, inputImageFileSchema, isArrayType, isAuthCollection, isEditorField, isFieldRequired, isFileFieldByName, isGeoPointType, isMultipleRelationField, isPermissionSchema, isRelationField, isSingleRelationField, isSystemCollection, isTemplateConfig, jsonField, loadBaseMigration, loadConfig, loadSnapshot, loadSnapshotIfExists, loadSnapshotWithMigrations, logError, logInfo, logSection, logSuccess, logWarning, mapZodArrayType, mapZodBooleanType, mapZodDateType, mapZodEnumType, mapZodNumberType, mapZodRecordType, mapZodStringType, mapZodTypeToPocketBase, matchCollectionsByName, matchFieldsByName, mergePermissions, mergeSnapshots, numberField, omitImageFilesSchema, parseSchemaFiles, pluralize, requiresForceFlag, resolveTargetCollection, resolveTemplate, saveSnapshot, selectField, selectSchemaForCollection, singularize, snapshotExists, textField, toCollectionName, unwrapZodType, urlField, validatePermissionConfig, validateRuleExpression, validateSnapshot, withIndexes, withPermissions, withProgress, writeMigrationFile };
5323
5454
  //# sourceMappingURL=index.js.map
5324
5455
  //# sourceMappingURL=index.js.map