@rebasepro/server-postgres 0.12.0 → 0.12.1-canary.g009ed95

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 (91) hide show
  1. package/dist/PostgresBackendDriver.d.ts +1 -1
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/backup/backup-service.d.ts +10 -1
  5. package/dist/backup/pg-tools.d.ts +47 -0
  6. package/dist/backup-service-CD8o_1Sl.js +8999 -0
  7. package/dist/backup-service-CD8o_1Sl.js.map +1 -0
  8. package/dist/cli-helpers.d.ts +39 -0
  9. package/dist/connection-BuZ97wsr.js +250 -0
  10. package/dist/connection-BuZ97wsr.js.map +1 -0
  11. package/dist/connection.d.ts +42 -0
  12. package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
  13. package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
  14. package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
  15. package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
  16. package/dist/index.es.js +816 -9679
  17. package/dist/index.es.js.map +1 -1
  18. package/dist/policy-CeA1JcxP.js +105 -0
  19. package/dist/policy-CeA1JcxP.js.map +1 -0
  20. package/dist/schema/auth-schema.d.ts +83 -144
  21. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  22. package/dist/schema/ensure-collection-tables.d.ts +44 -2
  23. package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
  24. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  25. package/dist/schema/introspect-db-logic.d.ts +94 -5
  26. package/dist/schema/introspect-db-queries.d.ts +119 -0
  27. package/dist/schema/introspect-db-structure.d.ts +263 -0
  28. package/dist/schema/introspect-db-types.d.ts +11 -0
  29. package/dist/services/FetchService.d.ts +4 -1
  30. package/dist/services/RelationService.d.ts +24 -1
  31. package/dist/services/channel-bus/index.d.ts +1 -7
  32. package/dist/services/collection-helpers.d.ts +24 -1
  33. package/dist/services/dataService.d.ts +3 -1
  34. package/dist/services/row-pipeline.d.ts +1 -1
  35. package/dist/{src-BbFOPJ1S.js → src-CzbghKwf.js} +271 -173
  36. package/dist/src-CzbghKwf.js.map +1 -0
  37. package/dist/{src-Zqwaw3P5.js → src-DoU9yPqq.js} +3 -159
  38. package/dist/src-DoU9yPqq.js.map +1 -0
  39. package/dist/utils/connection-string.d.ts +29 -0
  40. package/dist/utils/drizzle-conditions.d.ts +5 -4
  41. package/dist/utils/pg-error-utils.d.ts +19 -0
  42. package/dist/websocket-B2LsrINK.js +530 -0
  43. package/dist/websocket-B2LsrINK.js.map +1 -0
  44. package/package.json +14 -14
  45. package/src/PostgresAdapter.ts +21 -2
  46. package/src/PostgresBackendDriver.ts +4 -0
  47. package/src/PostgresBootstrapper.ts +192 -33
  48. package/src/auth/ensure-tables.ts +164 -9
  49. package/src/auth/services.ts +24 -2
  50. package/src/backup/backup-cli.ts +41 -2
  51. package/src/backup/backup-service.ts +38 -5
  52. package/src/backup/pg-tools.ts +96 -3
  53. package/src/cli-helpers.ts +70 -0
  54. package/src/cli.ts +44 -26
  55. package/src/collections/validate-relations.ts +15 -0
  56. package/src/connection.ts +73 -0
  57. package/src/data-transformer.ts +9 -3
  58. package/src/databasePoolManager.ts +5 -2
  59. package/src/schema/auth-schema.ts +30 -19
  60. package/src/schema/ensure-collection-policies.ts +105 -0
  61. package/src/schema/ensure-collection-tables.test.ts +105 -9
  62. package/src/schema/ensure-collection-tables.ts +220 -32
  63. package/src/schema/generate-drizzle-schema-logic.ts +23 -6
  64. package/src/schema/generate-postgres-ddl-logic.ts +382 -19
  65. package/src/schema/introspect-db-constraints.ts +385 -0
  66. package/src/schema/introspect-db-inference.ts +18 -8
  67. package/src/schema/introspect-db-logic.ts +385 -71
  68. package/src/schema/introspect-db-queries.ts +326 -0
  69. package/src/schema/introspect-db-structure.ts +670 -0
  70. package/src/schema/introspect-db-types.ts +56 -0
  71. package/src/schema/introspect-db.ts +37 -80
  72. package/src/schema/introspect-runtime.test.ts +56 -8
  73. package/src/schema/introspect-runtime.ts +31 -9
  74. package/src/security/policy-drift.test.ts +11 -3
  75. package/src/services/FetchService.ts +76 -14
  76. package/src/services/PersistService.ts +20 -6
  77. package/src/services/RelationService.ts +249 -48
  78. package/src/services/channel-bus/index.ts +0 -9
  79. package/src/services/collection-helpers.ts +40 -1
  80. package/src/services/dataService.ts +3 -1
  81. package/src/services/realtimeService.ts +3 -3
  82. package/src/services/row-pipeline.ts +1 -1
  83. package/src/utils/connection-string.ts +58 -0
  84. package/src/utils/drizzle-conditions.ts +31 -6
  85. package/src/utils/pg-error-utils.ts +46 -0
  86. package/src/websocket.ts +18 -9
  87. package/dist/chunk-DSJWtz9O.js +0 -40
  88. package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
  89. package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
  90. package/dist/src-BbFOPJ1S.js.map +0 -1
  91. package/dist/src-Zqwaw3P5.js.map +0 -1
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Reading validation rules out of CHECK constraints.
3
+ *
4
+ * A CHECK constraint is the schema author stating a rule the database already
5
+ * enforces. Introspection has never read them, so a generated form let the user
6
+ * type a value the database was always going to reject — the rule was written
7
+ * down, in the schema, and the UI asked anyway.
8
+ *
9
+ * Everything here parses the *normalized* text `pg_get_constraintdef` produces,
10
+ * not what the author typed: Postgres re-renders the expression from its parse
11
+ * tree, so `CHECK (price > 0)` on a `numeric` column always comes back as
12
+ * `CHECK ((price > (0)::numeric))`. That normalization is what makes a
13
+ * string-level parser tractable — the input is generated, and the shapes are
14
+ * few.
15
+ *
16
+ * Constraints this cannot read are skipped in full. A partially-understood
17
+ * constraint is worse than an unread one: it would produce validation that
18
+ * *narrows* differently from the database, so a value the UI accepts still
19
+ * fails on write, or — worse — a value the database allows is refused in the
20
+ * form with no way to see why.
21
+ *
22
+ * Pure module: no I/O, no logging.
23
+ */
24
+ import type { CheckConstraintRow } from "./introspect-db-logic";
25
+
26
+ /** What a table's CHECK constraints say about one column. */
27
+ export interface ColumnCheckFacts {
28
+ /** Allowed values, from `IN (…)` / `= ANY (ARRAY[…])` / `= 'literal'`. */
29
+ enumValues?: string[];
30
+ /** `x >= n` */
31
+ min?: number;
32
+ /** `x <= n` */
33
+ max?: number;
34
+ /** `x > n` */
35
+ moreThan?: number;
36
+ /** `x < n` */
37
+ lessThan?: number;
38
+ /** `length(x) >= n` */
39
+ lengthMin?: number;
40
+ /** `length(x) <= n` */
41
+ lengthMax?: number;
42
+ }
43
+
44
+ /** Per-table, per-column facts. */
45
+ export type CheckFactsByTable = Map<string, Map<string, ColumnCheckFacts>>;
46
+
47
+ /** The length functions whose argument is the column being constrained. */
48
+ const LENGTH_FUNCTIONS = new Set(["length", "char_length", "character_length", "octet_length"]);
49
+
50
+ /**
51
+ * Splits on a separator that appears at paren depth 0 and outside string
52
+ * literals.
53
+ *
54
+ * Depth tracking is the whole job: `ARRAY['a', 'b']` contains a comma that must
55
+ * not split the enclosing argument list, and `'it''s'` contains a quote that
56
+ * must not end the literal.
57
+ */
58
+ function splitTopLevel(input: string, separator: string): string[] {
59
+ const parts: string[] = [];
60
+ let depth = 0;
61
+ let inString = false;
62
+ let current = "";
63
+
64
+ for (let i = 0; i < input.length; i++) {
65
+ const ch = input[i];
66
+
67
+ if (inString) {
68
+ current += ch;
69
+ if (ch === "'") {
70
+ // '' inside a literal is an escaped quote, not the end of it.
71
+ if (input[i + 1] === "'") {
72
+ current += input[++i];
73
+ } else {
74
+ inString = false;
75
+ }
76
+ }
77
+ continue;
78
+ }
79
+
80
+ if (ch === "'") {
81
+ inString = true;
82
+ current += ch;
83
+ continue;
84
+ }
85
+ if (ch === "(" || ch === "[") depth++;
86
+ if (ch === ")" || ch === "]") depth--;
87
+
88
+ if (depth === 0 && input.startsWith(separator, i)) {
89
+ // A word separator must not match inside an identifier: "AND" is a
90
+ // separator in `a AND b` but not in `brand = 1`.
91
+ const before = input[i - 1];
92
+ const after = input[i + separator.length];
93
+ const wordish = /[A-Za-z]/.test(separator[0]);
94
+ const boundedOk = !wordish || ((before === undefined || !/[A-Za-z0-9_]/.test(before)) &&
95
+ (after === undefined || !/[A-Za-z0-9_]/.test(after)));
96
+ if (boundedOk) {
97
+ parts.push(current);
98
+ current = "";
99
+ i += separator.length - 1;
100
+ continue;
101
+ }
102
+ }
103
+
104
+ current += ch;
105
+ }
106
+
107
+ parts.push(current);
108
+ return parts.map((p) => p.trim()).filter((p) => p.length > 0);
109
+ }
110
+
111
+ /** Removes one layer of wrapping parentheses, repeatedly, when balanced. */
112
+ export function unwrapParens(input: string): string {
113
+ let s = input.trim();
114
+ while (s.startsWith("(") && s.endsWith(")")) {
115
+ let depth = 0;
116
+ let balanced = true;
117
+ let inString = false;
118
+ for (let i = 0; i < s.length; i++) {
119
+ const ch = s[i];
120
+ if (inString) {
121
+ if (ch === "'") {
122
+ if (s[i + 1] === "'") i++;
123
+ else inString = false;
124
+ }
125
+ continue;
126
+ }
127
+ if (ch === "'") { inString = true; continue; }
128
+ if (ch === "(") depth++;
129
+ else if (ch === ")") {
130
+ depth--;
131
+ // The opening paren closed before the end, so the outer pair is
132
+ // not a wrapper: `(a) AND (b)`.
133
+ if (depth === 0 && i < s.length - 1) { balanced = false; break; }
134
+ }
135
+ }
136
+ if (!balanced) break;
137
+ s = s.slice(1, -1).trim();
138
+ }
139
+ return s;
140
+ }
141
+
142
+ /** Drops trailing `::type` casts, including array and quoted forms. */
143
+ export function stripCasts(input: string): string {
144
+ let s = unwrapParens(input);
145
+ let previous: string;
146
+ do {
147
+ previous = s;
148
+ s = unwrapParens(
149
+ s.replace(/::\s*"?[A-Za-z_][A-Za-z0-9_ ]*"?(\s*\(\s*\d+\s*(,\s*\d+\s*)?\))?(\s*\[\s*\])*\s*$/, "").trim()
150
+ );
151
+ } while (s !== previous);
152
+ return s;
153
+ }
154
+
155
+ /** A bare, unquoted column reference — anything else is an expression. */
156
+ function asColumnName(input: string): string | null {
157
+ const s = stripCasts(input);
158
+ if (/^[a-z_][a-z0-9_$]*$/i.test(s)) return s;
159
+ // A quoted identifier keeps its case: "userId".
160
+ const quoted = s.match(/^"([^"]+)"$/);
161
+ return quoted ? quoted[1] : null;
162
+ }
163
+
164
+ /** `length(col)` and friends → the column they measure. */
165
+ function asLengthOfColumn(input: string): string | null {
166
+ const s = stripCasts(input);
167
+ const match = s.match(/^([a-z_]+)\s*\((.*)\)$/is);
168
+ if (!match || !LENGTH_FUNCTIONS.has(match[1].toLowerCase())) return null;
169
+ return asColumnName(match[2]);
170
+ }
171
+
172
+ /** A numeric literal, after casts are removed. Rejects anything else. */
173
+ function asNumber(input: string): number | null {
174
+ const s = stripCasts(input).replace(/^'(.*)'$/s, "$1");
175
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)$/.test(s)) return null;
176
+ const n = Number(s);
177
+ return Number.isFinite(n) ? n : null;
178
+ }
179
+
180
+ /** A single-quoted string literal, with `''` unescaped. */
181
+ function asStringLiteral(input: string): string | null {
182
+ const s = stripCasts(input);
183
+ if (!s.startsWith("'") || !s.endsWith("'") || s.length < 2) return null;
184
+ const body = s.slice(1, -1);
185
+ // Any unescaped quote inside means this was not one literal.
186
+ if (/(^|[^'])'($|[^'])/.test(body)) return null;
187
+ return body.replace(/''/g, "'");
188
+ }
189
+
190
+ /** The element list of `ARRAY[…]`, or of a parenthesised `IN (…)` list. */
191
+ function asValueList(input: string): string[] | null {
192
+ const s = stripCasts(input);
193
+ const arrayMatch = s.match(/^ARRAY\s*\[(.*)\]$/is);
194
+ const body = arrayMatch ? arrayMatch[1] : (s.startsWith("(") && s.endsWith(")") ? s.slice(1, -1) : null);
195
+ if (body === null) return null;
196
+ if (body.trim() === "") return null;
197
+
198
+ const values: string[] = [];
199
+ for (const element of splitTopLevel(body, ",")) {
200
+ const literal = asStringLiteral(element);
201
+ if (literal !== null) {
202
+ values.push(literal);
203
+ continue;
204
+ }
205
+ const numeric = asNumber(element);
206
+ if (numeric !== null) {
207
+ values.push(String(numeric));
208
+ continue;
209
+ }
210
+ return null; // A non-literal element means this is not an enumeration.
211
+ }
212
+ return values;
213
+ }
214
+
215
+ /** One understood comparison, already resolved to the column it constrains. */
216
+ interface CheckTerm {
217
+ column: string;
218
+ apply: (facts: ColumnCheckFacts) => void;
219
+ }
220
+
221
+ /**
222
+ * Parses one conjunct. Returns null when the shape is not one of the few this
223
+ * understands, which is the signal to abandon the whole constraint.
224
+ */
225
+ function parseTerm(rawTerm: string): CheckTerm | null {
226
+ const term = unwrapParens(rawTerm);
227
+
228
+ // ── Membership: `x = ANY (ARRAY[…])`, `x IN (…)` ──────────────────
229
+ const anyMatch = term.match(/^(.*?)\s*=\s*ANY\s*(\(.*\))$/is);
230
+ const inMatch = anyMatch ? null : term.match(/^(.*?)\s+IN\s*(\(.*\))$/is);
231
+ const membership = anyMatch ?? inMatch;
232
+ if (membership) {
233
+ const column = asColumnName(membership[1]);
234
+ const values = asValueList(membership[2]);
235
+ if (!column || !values || values.length === 0) return null;
236
+ return {
237
+ column,
238
+ apply: (facts) => {
239
+ // Two membership checks on one column mean both must hold.
240
+ facts.enumValues = facts.enumValues
241
+ ? facts.enumValues.filter((v) => values.includes(v))
242
+ : values;
243
+ }
244
+ };
245
+ }
246
+
247
+ // ── Comparison: `x >= n`, `length(x) <= n`, `x = 'literal'` ───────
248
+ // Longest operators first so `>=` is not read as `>`.
249
+ for (const operator of [">=", "<=", "<>", "!=", "=", ">", "<"] as const) {
250
+ const parts = splitTopLevel(term, operator);
251
+ if (parts.length !== 2) continue;
252
+ const [rawLeft, rawRight] = parts;
253
+
254
+ const lengthColumn = asLengthOfColumn(rawLeft);
255
+ if (lengthColumn) {
256
+ const bound = asNumber(rawRight);
257
+ if (bound === null) return null;
258
+ switch (operator) {
259
+ case ">=": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound, bound); } };
260
+ case ">": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound + 1, bound + 1); } };
261
+ case "<=": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound, bound); } };
262
+ case "<": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound - 1, bound - 1); } };
263
+ case "=": return { column: lengthColumn, apply: (f) => { f.lengthMin = bound; f.lengthMax = bound; } };
264
+ default: return null;
265
+ }
266
+ }
267
+
268
+ const column = asColumnName(rawLeft);
269
+ if (!column) continue;
270
+
271
+ if (operator === "=") {
272
+ const literal = asStringLiteral(rawRight);
273
+ if (literal !== null) {
274
+ return {
275
+ column,
276
+ apply: (facts) => {
277
+ facts.enumValues = facts.enumValues
278
+ ? facts.enumValues.filter((v) => v === literal)
279
+ : [literal];
280
+ }
281
+ };
282
+ }
283
+ // `x = 5` pins a number. Expressed as both bounds rather than as a
284
+ // one-value enum, which for a number would render as a dropdown.
285
+ const pinned = asNumber(rawRight);
286
+ if (pinned === null) return null;
287
+ return { column, apply: (f) => { f.min = pinned; f.max = pinned; } };
288
+ }
289
+
290
+ const bound = asNumber(rawRight);
291
+ if (bound === null) return null;
292
+ switch (operator) {
293
+ case ">=": return { column, apply: (f) => { f.min = Math.max(f.min ?? bound, bound); } };
294
+ case ">": return { column, apply: (f) => { f.moreThan = Math.max(f.moreThan ?? bound, bound); } };
295
+ case "<=": return { column, apply: (f) => { f.max = Math.min(f.max ?? bound, bound); } };
296
+ case "<": return { column, apply: (f) => { f.lessThan = Math.min(f.lessThan ?? bound, bound); } };
297
+ default: return null; // `<>` / `!=` exclude rather than bound.
298
+ }
299
+ }
300
+
301
+ return null;
302
+ }
303
+
304
+ /**
305
+ * Reads one constraint definition into per-column facts.
306
+ *
307
+ * Returns an empty map for anything not understood — including a constraint
308
+ * that is understood but spans two columns (`start_date < end_date`), which no
309
+ * per-property validation rule can express.
310
+ */
311
+ export function parseCheckDefinition(definition: string): Map<string, ColumnCheckFacts> {
312
+ const result = new Map<string, ColumnCheckFacts>();
313
+
314
+ const body = definition
315
+ .trim()
316
+ .replace(/\s+NOT\s+VALID\s*$/i, "")
317
+ .replace(/^CHECK\s*/i, "")
318
+ .trim();
319
+ if (body === definition.trim()) return result; // Not a CHECK definition.
320
+
321
+ const expression = unwrapParens(body);
322
+ if (!expression) return result;
323
+
324
+ // Only conjunctions. A disjunction (`a OR b`) does not narrow a column: each
325
+ // branch may allow what the other forbids, so no single bound follows.
326
+ if (splitTopLevel(expression, " OR ").length > 1) return result;
327
+
328
+ const terms = splitTopLevel(expression, " AND ");
329
+ const parsed: CheckTerm[] = [];
330
+ for (const term of terms) {
331
+ const one = parseTerm(term);
332
+ if (!one) return result; // One unreadable conjunct voids the constraint.
333
+ parsed.push(one);
334
+ }
335
+
336
+ // A CHECK naming two columns constrains their *relationship*; keeping the
337
+ // half that mentions one column would assert something the database does not.
338
+ const columns = new Set(parsed.map((t) => t.column));
339
+ if (columns.size !== 1) return result;
340
+
341
+ const facts: ColumnCheckFacts = {};
342
+ for (const term of parsed) term.apply(facts);
343
+ if (Object.keys(facts).length === 0) return result;
344
+
345
+ result.set(parsed[0].column, facts);
346
+ return result;
347
+ }
348
+
349
+ /** Merges every readable CHECK in the schema into per-table, per-column facts. */
350
+ export function parseCheckConstraints(rows: CheckConstraintRow[]): CheckFactsByTable {
351
+ const byTable: CheckFactsByTable = new Map();
352
+
353
+ for (const row of rows) {
354
+ const parsed = parseCheckDefinition(row.definition);
355
+ if (parsed.size === 0) continue;
356
+
357
+ let table = byTable.get(row.table_name);
358
+ if (!table) {
359
+ table = new Map();
360
+ byTable.set(row.table_name, table);
361
+ }
362
+
363
+ for (const [column, facts] of parsed) {
364
+ const existing = table.get(column);
365
+ if (!existing) {
366
+ table.set(column, facts);
367
+ continue;
368
+ }
369
+ // Separate constraints on the same column all hold at once.
370
+ if (facts.enumValues) {
371
+ existing.enumValues = existing.enumValues
372
+ ? existing.enumValues.filter((v) => facts.enumValues!.includes(v))
373
+ : facts.enumValues;
374
+ }
375
+ if (facts.min !== undefined) existing.min = Math.max(existing.min ?? facts.min, facts.min);
376
+ if (facts.max !== undefined) existing.max = Math.min(existing.max ?? facts.max, facts.max);
377
+ if (facts.moreThan !== undefined) existing.moreThan = Math.max(existing.moreThan ?? facts.moreThan, facts.moreThan);
378
+ if (facts.lessThan !== undefined) existing.lessThan = Math.min(existing.lessThan ?? facts.lessThan, facts.lessThan);
379
+ if (facts.lengthMin !== undefined) existing.lengthMin = Math.max(existing.lengthMin ?? facts.lengthMin, facts.lengthMin);
380
+ if (facts.lengthMax !== undefined) existing.lengthMax = Math.min(existing.lengthMax ?? facts.lengthMax, facts.lengthMax);
381
+ }
382
+ }
383
+
384
+ return byTable;
385
+ }
@@ -19,6 +19,10 @@ export function inferPropertyFromData(
19
19
  ): InferenceResult {
20
20
  const result: InferenceResult = {};
21
21
  const extraLines: string[] = [];
22
+ // Collected separately and emitted as one block at the end. Pushed inline,
23
+ // two admin-owned options from different branches produced two `admin: {`
24
+ // blocks in one property — a duplicate key, which is also a compile error.
25
+ const adminOptions: string[] = [];
22
26
 
23
27
  // Filter out null/undefined for analysis
24
28
  const validValues = sampleValues.filter(v => v !== null && v !== undefined && v !== "");
@@ -189,10 +193,14 @@ export function inferPropertyFromData(
189
193
  }
190
194
  }
191
195
 
196
+ // `multiline` and `markdown` are *admin* property options, so they have
197
+ // to sit inside the `admin` block. Emitted at the top of the property —
198
+ // where they were — `PostgresCollectionConfig` does not declare them and
199
+ // the generated file does not compile.
192
200
  if (hasMarkdown) {
193
- extraLines.push(" multiline: true,\n markdown: true");
201
+ adminOptions.push("multiline: true", "markdown: true");
194
202
  } else if (hasNewlines || maxLength > 100) {
195
- extraLines.push(" multiline: true");
203
+ adminOptions.push("multiline: true");
196
204
  }
197
205
 
198
206
  if (maxLength > 0 && maxLength < 10000) { // arbitrary cap to avoid huge limits
@@ -212,7 +220,8 @@ export function inferPropertyFromData(
212
220
  if (allAbsoluteUrls) {
213
221
  const isImage = validValues.some(v => typeof v === "string" && v.match(/\.(jpeg|jpg|gif|png|webp|svg)/i));
214
222
  if (isImage || isMedia) {
215
- extraLines.push(" url: true,\n admin: {\n urlPreview: \"image\"\n }");
223
+ extraLines.push(" url: true");
224
+ adminOptions.push("urlPreview: \"image\"");
216
225
  } else {
217
226
  extraLines.push(" url: true");
218
227
  }
@@ -224,15 +233,16 @@ export function inferPropertyFromData(
224
233
  const inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : "files";
225
234
  extraLines.push(` storage: {\n storagePath: "${inferredStoragePath}"\n }`);
226
235
  } else if (isUrl) {
227
- if (isMedia) {
228
- extraLines.push(" url: true,\n admin: {\n urlPreview: \"image\"\n }");
229
- } else {
230
- extraLines.push(" url: true");
231
- }
236
+ extraLines.push(" url: true");
237
+ if (isMedia) adminOptions.push("urlPreview: \"image\"");
232
238
  }
233
239
  }
234
240
  }
235
241
 
242
+ if (adminOptions.length > 0) {
243
+ extraLines.push(` admin: {\n${adminOptions.map((o) => ` ${o}`).join(",\n")}\n }`);
244
+ }
245
+
236
246
  result.extra = extraLines.length > 0 ? "\n" + extraLines.join(",\n") + "," : "";
237
247
  return result;
238
248
  }