pocketbase-zod-schema 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cli/index.cjs +36 -32
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.d.cts +2 -2
  5. package/dist/cli/index.d.ts +2 -2
  6. package/dist/cli/index.js +36 -32
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/migrate.cjs +36 -32
  9. package/dist/cli/migrate.cjs.map +1 -1
  10. package/dist/cli/migrate.js +36 -32
  11. package/dist/cli/migrate.js.map +1 -1
  12. package/dist/cli/utils/index.d.cts +2 -2
  13. package/dist/cli/utils/index.d.ts +2 -2
  14. package/dist/{fields-RVj26U-O.d.cts → fields-DBBm06VU.d.cts} +34 -7
  15. package/dist/{fields-RVj26U-O.d.ts → fields-DBBm06VU.d.ts} +34 -7
  16. package/dist/index.cjs +125 -66
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +125 -66
  21. package/dist/index.js.map +1 -1
  22. package/dist/migration/analyzer.cjs.map +1 -1
  23. package/dist/migration/analyzer.d.cts +2 -2
  24. package/dist/migration/analyzer.d.ts +2 -2
  25. package/dist/migration/analyzer.js.map +1 -1
  26. package/dist/migration/diff.d.cts +2 -2
  27. package/dist/migration/diff.d.ts +2 -2
  28. package/dist/migration/generator.cjs +70 -60
  29. package/dist/migration/generator.cjs.map +1 -1
  30. package/dist/migration/generator.d.cts +2 -2
  31. package/dist/migration/generator.d.ts +2 -2
  32. package/dist/migration/generator.js +70 -60
  33. package/dist/migration/generator.js.map +1 -1
  34. package/dist/migration/index.cjs +70 -60
  35. package/dist/migration/index.cjs.map +1 -1
  36. package/dist/migration/index.d.cts +3 -3
  37. package/dist/migration/index.d.ts +3 -3
  38. package/dist/migration/index.js +70 -60
  39. package/dist/migration/index.js.map +1 -1
  40. package/dist/migration/snapshot.d.cts +2 -2
  41. package/dist/migration/snapshot.d.ts +2 -2
  42. package/dist/migration/utils/index.cjs.map +1 -1
  43. package/dist/migration/utils/index.d.cts +2 -2
  44. package/dist/migration/utils/index.d.ts +2 -2
  45. package/dist/migration/utils/index.js.map +1 -1
  46. package/dist/schema.cjs +55 -6
  47. package/dist/schema.cjs.map +1 -1
  48. package/dist/schema.d.cts +1 -1
  49. package/dist/schema.d.ts +1 -1
  50. package/dist/schema.js +55 -6
  51. package/dist/schema.js.map +1 -1
  52. package/dist/{type-mapper-DaBe-1ph.d.cts → type-mapper-DsGgZwUo.d.cts} +1 -1
  53. package/dist/{type-mapper-CZzVeDj7.d.ts → type-mapper-Dvh4QTM-.d.ts} +1 -1
  54. package/dist/{types-D-Fsdn_O.d.cts → types-CVxPCgWX.d.cts} +1 -1
  55. package/dist/{types-CUVzgZ9k.d.ts → types-Dfp-NP2D.d.ts} +1 -1
  56. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { Ora } from 'ora';
2
- import { e as SchemaDiff } from '../../types-D-Fsdn_O.cjs';
3
- import '../../fields-RVj26U-O.cjs';
2
+ import { e as SchemaDiff } from '../../types-CVxPCgWX.cjs';
3
+ import '../../fields-DBBm06VU.cjs';
4
4
  import 'zod';
5
5
  import '../../permissions-ZHafVSIx.cjs';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { Ora } from 'ora';
2
- import { e as SchemaDiff } from '../../types-CUVzgZ9k.js';
3
- import '../../fields-RVj26U-O.js';
2
+ import { e as SchemaDiff } from '../../types-Dfp-NP2D.js';
3
+ import '../../fields-DBBm06VU.js';
4
4
  import 'zod';
5
5
  import '../../permissions-ZHafVSIx.js';
6
6
 
@@ -116,6 +116,15 @@ interface SelectFieldOptions {
116
116
  */
117
117
  maxSelect?: number;
118
118
  }
119
+ /**
120
+ * Human-friendly byte size input.
121
+ *
122
+ * - Use a number for raw bytes (e.g. `5242880`)
123
+ * - Use a string with unit suffix for kibibytes/mebibytes/gibibytes (e.g. `"5M"`, `"1G"`)
124
+ *
125
+ * Supported suffixes: `K`, `M`, `G` (case-insensitive).
126
+ */
127
+ type ByteSize = number | `${number}${"K" | "M" | "G" | "k" | "m" | "g"}`;
119
128
  /**
120
129
  * File field configuration options
121
130
  */
@@ -126,9 +135,19 @@ interface FileFieldOptions {
126
135
  */
127
136
  mimeTypes?: string[];
128
137
  /**
129
- * Maximum file size in bytes
138
+ * Maximum file size.
139
+ *
140
+ * - Provide a number for raw bytes
141
+ * - Or use a string with `K`, `M`, `G` suffix (case-insensitive)
142
+ *
143
+ * Max allowed is `8G`.
144
+ *
145
+ * @example
146
+ * maxSize: 5242880
147
+ * maxSize: "5M"
148
+ * maxSize: "1G"
130
149
  */
131
- maxSize?: number;
150
+ maxSize?: ByteSize;
132
151
  /**
133
152
  * Thumbnail sizes to generate
134
153
  * Example: ["100x100", "200x200"]
@@ -299,29 +318,37 @@ declare function SelectField<T extends [string, ...string[]]>(values: T, options
299
318
  * Maps to PocketBase 'file' field type with maxSelect=1
300
319
  *
301
320
  * @param options - Optional file constraints
302
- * @returns Zod File schema with PocketBase metadata
321
+ * @returns Zod schema that accepts File on input and returns string when reading from database
303
322
  *
304
323
  * @example
305
324
  * const ProductSchema = z.object({
306
325
  * thumbnail: FileField({ mimeTypes: ["image/*"], maxSize: 5242880 }),
307
326
  * document: FileField({ mimeTypes: ["application/pdf"] }),
308
327
  * });
328
+ *
329
+ * @remarks
330
+ * - When creating/updating records: accepts File objects
331
+ * - When reading from PocketBase: returns string (filename)
309
332
  */
310
- declare function FileField(options?: FileFieldOptions): z.ZodType<File>;
333
+ declare function FileField(options?: FileFieldOptions): z.ZodType<string, z.ZodTypeDef, File | string>;
311
334
  /**
312
335
  * Creates a multiple files field schema
313
336
  * Maps to PocketBase 'file' field type with maxSelect>1
314
337
  *
315
338
  * @param options - Optional file constraints
316
- * @returns Zod array of File schema with PocketBase metadata
339
+ * @returns Zod array schema that accepts File[] on input and returns string[] when reading from database
317
340
  *
318
341
  * @example
319
342
  * const ProductSchema = z.object({
320
343
  * images: FilesField({ mimeTypes: ["image/*"], maxSelect: 5 }),
321
344
  * attachments: FilesField({ minSelect: 1, maxSelect: 10 }),
322
345
  * });
346
+ *
347
+ * @remarks
348
+ * - When creating/updating records: accepts File[]
349
+ * - When reading from PocketBase: returns string[] (filenames)
323
350
  */
324
- declare function FilesField(options?: FilesFieldOptions): z.ZodArray<z.ZodType<File>>;
351
+ declare function FilesField(options?: FilesFieldOptions): z.ZodType<string[], z.ZodTypeDef, (File | string)[]>;
325
352
  /**
326
353
  * Creates a JSON field schema with optional inner schema validation
327
354
  * Maps to PocketBase 'json' field type
@@ -362,4 +389,4 @@ declare function GeoPointField(): z.ZodObject<{
362
389
  lat: z.ZodNumber;
363
390
  }>;
364
391
 
365
- export { type AutodateFieldOptions as A, BoolField as B, type DateFieldOptions as D, EmailField as E, FIELD_METADATA_KEY as F, GeoPointField as G, JSONField as J, type NumberFieldOptions as N, type PocketBaseFieldType as P, type SelectFieldOptions as S, type TextFieldOptions as T, URLField as U, type FieldMetadata as a, type FileFieldOptions as b, type FilesFieldOptions as c, NumberField as d, extractFieldMetadata as e, TextField as f, EditorField as g, DateField as h, AutodateField as i, SelectField as j, FileField as k, FilesField as l };
392
+ export { type AutodateFieldOptions as A, type ByteSize as B, type DateFieldOptions as D, EmailField as E, FIELD_METADATA_KEY as F, GeoPointField as G, JSONField as J, type NumberFieldOptions as N, type PocketBaseFieldType as P, type SelectFieldOptions as S, type TextFieldOptions as T, URLField as U, type FieldMetadata as a, type FileFieldOptions as b, type FilesFieldOptions as c, BoolField as d, extractFieldMetadata as e, NumberField as f, TextField as g, EditorField as h, DateField as i, AutodateField as j, SelectField as k, FileField as l, FilesField as m };
@@ -116,6 +116,15 @@ interface SelectFieldOptions {
116
116
  */
117
117
  maxSelect?: number;
118
118
  }
119
+ /**
120
+ * Human-friendly byte size input.
121
+ *
122
+ * - Use a number for raw bytes (e.g. `5242880`)
123
+ * - Use a string with unit suffix for kibibytes/mebibytes/gibibytes (e.g. `"5M"`, `"1G"`)
124
+ *
125
+ * Supported suffixes: `K`, `M`, `G` (case-insensitive).
126
+ */
127
+ type ByteSize = number | `${number}${"K" | "M" | "G" | "k" | "m" | "g"}`;
119
128
  /**
120
129
  * File field configuration options
121
130
  */
@@ -126,9 +135,19 @@ interface FileFieldOptions {
126
135
  */
127
136
  mimeTypes?: string[];
128
137
  /**
129
- * Maximum file size in bytes
138
+ * Maximum file size.
139
+ *
140
+ * - Provide a number for raw bytes
141
+ * - Or use a string with `K`, `M`, `G` suffix (case-insensitive)
142
+ *
143
+ * Max allowed is `8G`.
144
+ *
145
+ * @example
146
+ * maxSize: 5242880
147
+ * maxSize: "5M"
148
+ * maxSize: "1G"
130
149
  */
131
- maxSize?: number;
150
+ maxSize?: ByteSize;
132
151
  /**
133
152
  * Thumbnail sizes to generate
134
153
  * Example: ["100x100", "200x200"]
@@ -299,29 +318,37 @@ declare function SelectField<T extends [string, ...string[]]>(values: T, options
299
318
  * Maps to PocketBase 'file' field type with maxSelect=1
300
319
  *
301
320
  * @param options - Optional file constraints
302
- * @returns Zod File schema with PocketBase metadata
321
+ * @returns Zod schema that accepts File on input and returns string when reading from database
303
322
  *
304
323
  * @example
305
324
  * const ProductSchema = z.object({
306
325
  * thumbnail: FileField({ mimeTypes: ["image/*"], maxSize: 5242880 }),
307
326
  * document: FileField({ mimeTypes: ["application/pdf"] }),
308
327
  * });
328
+ *
329
+ * @remarks
330
+ * - When creating/updating records: accepts File objects
331
+ * - When reading from PocketBase: returns string (filename)
309
332
  */
310
- declare function FileField(options?: FileFieldOptions): z.ZodType<File>;
333
+ declare function FileField(options?: FileFieldOptions): z.ZodType<string, z.ZodTypeDef, File | string>;
311
334
  /**
312
335
  * Creates a multiple files field schema
313
336
  * Maps to PocketBase 'file' field type with maxSelect>1
314
337
  *
315
338
  * @param options - Optional file constraints
316
- * @returns Zod array of File schema with PocketBase metadata
339
+ * @returns Zod array schema that accepts File[] on input and returns string[] when reading from database
317
340
  *
318
341
  * @example
319
342
  * const ProductSchema = z.object({
320
343
  * images: FilesField({ mimeTypes: ["image/*"], maxSelect: 5 }),
321
344
  * attachments: FilesField({ minSelect: 1, maxSelect: 10 }),
322
345
  * });
346
+ *
347
+ * @remarks
348
+ * - When creating/updating records: accepts File[]
349
+ * - When reading from PocketBase: returns string[] (filenames)
323
350
  */
324
- declare function FilesField(options?: FilesFieldOptions): z.ZodArray<z.ZodType<File>>;
351
+ declare function FilesField(options?: FilesFieldOptions): z.ZodType<string[], z.ZodTypeDef, (File | string)[]>;
325
352
  /**
326
353
  * Creates a JSON field schema with optional inner schema validation
327
354
  * Maps to PocketBase 'json' field type
@@ -362,4 +389,4 @@ declare function GeoPointField(): z.ZodObject<{
362
389
  lat: z.ZodNumber;
363
390
  }>;
364
391
 
365
- export { type AutodateFieldOptions as A, BoolField as B, type DateFieldOptions as D, EmailField as E, FIELD_METADATA_KEY as F, GeoPointField as G, JSONField as J, type NumberFieldOptions as N, type PocketBaseFieldType as P, type SelectFieldOptions as S, type TextFieldOptions as T, URLField as U, type FieldMetadata as a, type FileFieldOptions as b, type FilesFieldOptions as c, NumberField as d, extractFieldMetadata as e, TextField as f, EditorField as g, DateField as h, AutodateField as i, SelectField as j, FileField as k, FilesField as l };
392
+ export { type AutodateFieldOptions as A, type ByteSize as B, type DateFieldOptions as D, EmailField as E, FIELD_METADATA_KEY as F, GeoPointField as G, JSONField as J, type NumberFieldOptions as N, type PocketBaseFieldType as P, type SelectFieldOptions as S, type TextFieldOptions as T, URLField as U, type FieldMetadata as a, type FileFieldOptions as b, type FilesFieldOptions as c, BoolField as d, extractFieldMetadata as e, NumberField as f, TextField as g, EditorField as h, DateField as i, AutodateField as j, SelectField as k, FileField as l, FilesField as m };
package/dist/index.cjs CHANGED
@@ -488,6 +488,45 @@ function extractFieldMetadata(description) {
488
488
  }
489
489
  return null;
490
490
  }
491
+ var MAX_FILE_SIZE_BYTES = 8 * 1024 * 1024 * 1024;
492
+ function parseByteSizeToBytes(value, context) {
493
+ let bytes;
494
+ if (typeof value === "number") {
495
+ if (!Number.isFinite(value)) {
496
+ throw new Error(`${context}: maxSize must be a finite number of bytes`);
497
+ }
498
+ bytes = Math.round(value);
499
+ } else {
500
+ const trimmed = value.trim();
501
+ const match = /^(\d+(?:\.\d+)?)\s*([KMG])$/i.exec(trimmed);
502
+ if (!match) {
503
+ throw new Error(`${context}: maxSize string must be like "10K", "5M", or "1G" (case-insensitive)`);
504
+ }
505
+ const amount = Number(match[1]);
506
+ const unit = match[2].toUpperCase();
507
+ if (!Number.isFinite(amount)) {
508
+ throw new Error(`${context}: maxSize must be a valid number`);
509
+ }
510
+ const multiplier = unit === "K" ? 1024 : unit === "M" ? 1024 * 1024 : 1024 * 1024 * 1024;
511
+ bytes = Math.round(amount * multiplier);
512
+ }
513
+ if (bytes < 0) {
514
+ throw new Error(`${context}: maxSize must be >= 0`);
515
+ }
516
+ if (bytes > MAX_FILE_SIZE_BYTES) {
517
+ throw new Error(`${context}: maxSize cannot exceed 8G (${MAX_FILE_SIZE_BYTES} bytes)`);
518
+ }
519
+ return bytes;
520
+ }
521
+ function normalizeFileFieldOptions(options, context) {
522
+ if (!options) return options;
523
+ if (options.maxSize === void 0) return options;
524
+ return {
525
+ ...options,
526
+ // PocketBase expects bytes; normalize any human-friendly inputs to bytes here.
527
+ maxSize: parseByteSizeToBytes(options.maxSize, context)
528
+ };
529
+ }
491
530
  function BoolField() {
492
531
  const metadata = {
493
532
  [FIELD_METADATA_KEY]: {
@@ -610,11 +649,14 @@ function SelectField(values, options) {
610
649
  return enumSchema.describe(JSON.stringify(metadata));
611
650
  }
612
651
  function FileField(options) {
613
- const schema = zod.z.instanceof(File);
652
+ const schema = zod.z.preprocess((val) => {
653
+ return val instanceof File ? val.name || "" : val;
654
+ }, zod.z.string());
655
+ const normalizedOptions = normalizeFileFieldOptions(options, "FileField");
614
656
  const metadata = {
615
657
  [FIELD_METADATA_KEY]: {
616
658
  type: "file",
617
- options: options || {}
659
+ options: normalizedOptions || {}
618
660
  }
619
661
  };
620
662
  return schema.describe(JSON.stringify(metadata));
@@ -625,17 +667,24 @@ function FilesField(options) {
625
667
  throw new Error("FilesField: minSelect cannot be greater than maxSelect");
626
668
  }
627
669
  }
628
- let schema = zod.z.array(zod.z.instanceof(File));
670
+ let baseArraySchema = zod.z.array(zod.z.string());
629
671
  if (options?.minSelect !== void 0) {
630
- schema = schema.min(options.minSelect);
672
+ baseArraySchema = baseArraySchema.min(options.minSelect);
631
673
  }
632
674
  if (options?.maxSelect !== void 0) {
633
- schema = schema.max(options.maxSelect);
675
+ baseArraySchema = baseArraySchema.max(options.maxSelect);
634
676
  }
677
+ const schema = zod.z.preprocess((val) => {
678
+ if (Array.isArray(val)) {
679
+ return val.map((item) => item instanceof File ? item.name || "" : item);
680
+ }
681
+ return val;
682
+ }, baseArraySchema);
683
+ const normalizedOptions = normalizeFileFieldOptions(options, "FilesField");
635
684
  const metadata = {
636
685
  [FIELD_METADATA_KEY]: {
637
686
  type: "file",
638
- options: options || {}
687
+ options: normalizedOptions || {}
639
688
  }
640
689
  };
641
690
  return schema.describe(JSON.stringify(metadata));
@@ -4195,7 +4244,7 @@ function formatValue(value) {
4195
4244
  return JSON.stringify(value).replace(/","/g, '", "');
4196
4245
  }
4197
4246
  if (typeof value === "object") {
4198
- const entries = Object.entries(value).map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ");
4247
+ const entries = Object.entries(value).filter(([_k, v]) => v !== void 0).map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ");
4199
4248
  return `{ ${entries} }`;
4200
4249
  }
4201
4250
  return String(value);
@@ -4489,11 +4538,9 @@ function generateFieldModification(collectionName, modification, varName, isLast
4489
4538
  function generateFieldDeletion(collectionName, fieldName, varName, isLast = false) {
4490
4539
  const lines = [];
4491
4540
  const collectionVar = varName || `collection_${collectionName}_${fieldName}`;
4492
- const fieldVar = `${collectionVar}_field`;
4493
4541
  lines.push(` const ${collectionVar} = app.findCollectionByNameOrId("${collectionName}");`);
4494
- lines.push(` const ${fieldVar} = ${collectionVar}.fields.getByName("${fieldName}");`);
4495
4542
  lines.push(``);
4496
- lines.push(` ${collectionVar}.fields.remove(${fieldVar}.id);`);
4543
+ lines.push(` ${collectionVar}.fields.removeByName("${fieldName}");`);
4497
4544
  lines.push(``);
4498
4545
  lines.push(isLast ? ` return app.save(${collectionVar});` : ` app.save(${collectionVar});`);
4499
4546
  return lines.join("\n");
@@ -4611,20 +4658,23 @@ function generateOperationUpMigration(operation, collectionIdMap) {
4611
4658
  lines.push(generateCollectionDeletion(collectionName, varName, true));
4612
4659
  }
4613
4660
  let code = lines.join("\n");
4614
- const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
4615
- const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
4616
- const saveMatches = [...code.matchAll(savePattern)];
4617
- const deleteMatches = [...code.matchAll(deletePattern)];
4618
- const allMatches = [
4619
- ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
4620
- ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
4621
- ].sort((a, b) => b.index - a.index);
4622
- if (allMatches.length > 0) {
4623
- const lastMatch = allMatches[0];
4624
- if (lastMatch.type === "save") {
4625
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4626
- } else {
4627
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4661
+ const hasReturnStatement = /return\s+app\.(save|delete)\(/m.test(code);
4662
+ if (!hasReturnStatement) {
4663
+ const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
4664
+ const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
4665
+ const saveMatches = [...code.matchAll(savePattern)];
4666
+ const deleteMatches = [...code.matchAll(deletePattern)];
4667
+ const allMatches = [
4668
+ ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
4669
+ ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
4670
+ ].sort((a, b) => b.index - a.index);
4671
+ if (allMatches.length > 0) {
4672
+ const lastMatch = allMatches[0];
4673
+ if (lastMatch.type === "save") {
4674
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4675
+ } else {
4676
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4677
+ }
4628
4678
  }
4629
4679
  }
4630
4680
  return code;
@@ -4713,20 +4763,23 @@ function generateOperationDownMigration(operation, collectionIdMap) {
4713
4763
  }
4714
4764
  }
4715
4765
  let code = lines.join("\n");
4716
- const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
4717
- const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
4718
- const saveMatches = [...code.matchAll(savePattern)];
4719
- const deleteMatches = [...code.matchAll(deletePattern)];
4720
- const allMatches = [
4721
- ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
4722
- ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
4723
- ].sort((a, b) => b.index - a.index);
4724
- if (allMatches.length > 0) {
4725
- const lastMatch = allMatches[0];
4726
- if (lastMatch.type === "save") {
4727
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4728
- } else {
4729
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4766
+ const hasReturnStatement = /return\s+app\.(save|delete)\(/m.test(code);
4767
+ if (!hasReturnStatement) {
4768
+ const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
4769
+ const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
4770
+ const saveMatches = [...code.matchAll(savePattern)];
4771
+ const deleteMatches = [...code.matchAll(deletePattern)];
4772
+ const allMatches = [
4773
+ ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
4774
+ ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
4775
+ ].sort((a, b) => b.index - a.index);
4776
+ if (allMatches.length > 0) {
4777
+ const lastMatch = allMatches[0];
4778
+ if (lastMatch.type === "save") {
4779
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4780
+ } else {
4781
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4782
+ }
4730
4783
  }
4731
4784
  }
4732
4785
  return code;
@@ -4827,20 +4880,23 @@ function generateUpMigration(diff) {
4827
4880
  lines.push(``);
4828
4881
  }
4829
4882
  let code = lines.join("\n");
4830
- const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
4831
- const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
4832
- const saveMatches = [...code.matchAll(savePattern)];
4833
- const deleteMatches = [...code.matchAll(deletePattern)];
4834
- const allMatches = [
4835
- ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
4836
- ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
4837
- ].sort((a, b) => b.index - a.index);
4838
- if (allMatches.length > 0) {
4839
- const lastMatch = allMatches[0];
4840
- if (lastMatch.type === "save") {
4841
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4842
- } else {
4843
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4883
+ const hasReturnStatement = /return\s+app\.(save|delete)\(/m.test(code);
4884
+ if (!hasReturnStatement) {
4885
+ const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
4886
+ const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
4887
+ const saveMatches = [...code.matchAll(savePattern)];
4888
+ const deleteMatches = [...code.matchAll(deletePattern)];
4889
+ const allMatches = [
4890
+ ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
4891
+ ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
4892
+ ].sort((a, b) => b.index - a.index);
4893
+ if (allMatches.length > 0) {
4894
+ const lastMatch = allMatches[0];
4895
+ if (lastMatch.type === "save") {
4896
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4897
+ } else {
4898
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4899
+ }
4844
4900
  }
4845
4901
  }
4846
4902
  return code;
@@ -4957,20 +5013,23 @@ function generateDownMigration(diff) {
4957
5013
  lines.push(``);
4958
5014
  }
4959
5015
  let code = lines.join("\n");
4960
- const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
4961
- const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
4962
- const saveMatches = [...code.matchAll(savePattern)];
4963
- const deleteMatches = [...code.matchAll(deletePattern)];
4964
- const allMatches = [
4965
- ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
4966
- ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
4967
- ].sort((a, b) => b.index - a.index);
4968
- if (allMatches.length > 0) {
4969
- const lastMatch = allMatches[0];
4970
- if (lastMatch.type === "save") {
4971
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
4972
- } else {
4973
- code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
5016
+ const hasReturnStatement = /return\s+app\.(save|delete)\(/m.test(code);
5017
+ if (!hasReturnStatement) {
5018
+ const savePattern = /^(\s*)app\.save\((\w+)\);$/gm;
5019
+ const deletePattern = /^(\s*)app\.delete\((\w+)\);$/gm;
5020
+ const saveMatches = [...code.matchAll(savePattern)];
5021
+ const deleteMatches = [...code.matchAll(deletePattern)];
5022
+ const allMatches = [
5023
+ ...saveMatches.map((m) => ({ match: m, type: "save", index: m.index })),
5024
+ ...deleteMatches.map((m) => ({ match: m, type: "delete", index: m.index }))
5025
+ ].sort((a, b) => b.index - a.index);
5026
+ if (allMatches.length > 0) {
5027
+ const lastMatch = allMatches[0];
5028
+ if (lastMatch.type === "save") {
5029
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.save(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
5030
+ } else {
5031
+ code = code.substring(0, lastMatch.match.index) + lastMatch.match[1] + "return app.delete(" + lastMatch.match[2] + ");" + code.substring(lastMatch.match.index + lastMatch.match[0].length);
5032
+ }
4974
5033
  }
4975
5034
  }
4976
5035
  return code;