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