@rebasepro/common 0.7.0 → 0.9.0

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 (63) hide show
  1. package/README.md +4 -4
  2. package/dist/collections/CollectionRegistry.d.ts +30 -15
  3. package/dist/collections/default-collections.d.ts +255 -2
  4. package/dist/data/buildRebaseData.d.ts +30 -2
  5. package/dist/data/buildRoutedRebaseData.d.ts +14 -9
  6. package/dist/data/filter-dialect.d.ts +75 -0
  7. package/dist/data/query_builder.d.ts +4 -4
  8. package/dist/data/resolveDataSource.d.ts +8 -8
  9. package/dist/data/sort-dialect.d.ts +41 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.es.js +1125 -299
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/index.umd.js +1138 -303
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/util/builders.d.ts +52 -42
  16. package/dist/util/callbacks.d.ts +8 -3
  17. package/dist/util/collections.d.ts +4 -4
  18. package/dist/util/entities.d.ts +2 -2
  19. package/dist/util/filter-operator-resolution.d.ts +32 -0
  20. package/dist/util/index.d.ts +2 -0
  21. package/dist/util/navigation_from_path.d.ts +4 -4
  22. package/dist/util/navigation_utils.d.ts +3 -3
  23. package/dist/util/parent_references_from_path.d.ts +2 -2
  24. package/dist/util/permissions.d.ts +30 -6
  25. package/dist/util/policy/evaluatePolicy.d.ts +31 -0
  26. package/dist/util/policy/index.d.ts +3 -0
  27. package/dist/util/policy/policyToPostgres.d.ts +22 -0
  28. package/dist/util/policy/securityRuleToConditions.d.ts +24 -0
  29. package/dist/util/policy/sqlToPolicy.d.ts +20 -0
  30. package/dist/util/references.d.ts +2 -2
  31. package/dist/util/relations.d.ts +5 -5
  32. package/dist/util/resolutions.d.ts +2 -2
  33. package/dist/util/storage.d.ts +26 -1
  34. package/package.json +13 -13
  35. package/src/collections/CollectionRegistry.ts +92 -61
  36. package/src/collections/default-collections.ts +4 -4
  37. package/src/data/buildRebaseData.ts +336 -172
  38. package/src/data/buildRoutedRebaseData.ts +22 -16
  39. package/src/data/filter-dialect.ts +403 -0
  40. package/src/data/query_builder.ts +19 -10
  41. package/src/data/resolveDataSource.ts +10 -10
  42. package/src/data/sort-dialect.ts +56 -0
  43. package/src/index.ts +2 -0
  44. package/src/util/builders.ts +87 -84
  45. package/src/util/callbacks.ts +15 -8
  46. package/src/util/collections.ts +4 -4
  47. package/src/util/entities.ts +4 -4
  48. package/src/util/filter-operator-resolution.ts +81 -0
  49. package/src/util/index.ts +2 -0
  50. package/src/util/navigation_from_path.ts +4 -4
  51. package/src/util/navigation_utils.ts +8 -8
  52. package/src/util/parent_references_from_path.ts +3 -3
  53. package/src/util/permissions.test.ts +7 -5
  54. package/src/util/permissions.ts +90 -163
  55. package/src/util/policy/evaluatePolicy.ts +152 -0
  56. package/src/util/policy/index.ts +3 -0
  57. package/src/util/policy/policyToPostgres.ts +165 -0
  58. package/src/util/policy/securityRuleToConditions.ts +67 -0
  59. package/src/util/policy/sqlToPolicy.ts +88 -0
  60. package/src/util/references.ts +3 -3
  61. package/src/util/relations.ts +19 -20
  62. package/src/util/resolutions.ts +11 -11
  63. package/src/util/storage.ts +34 -1
@@ -0,0 +1,67 @@
1
+ import { PolicyExpression, SecurityRule, policy } from "@rebasepro/types";
2
+ import { sqlToPolicy } from "./sqlToPolicy";
3
+
4
+ /**
5
+ * The normalized `USING` / `WITH CHECK` conditions for a single security rule,
6
+ * expressed in the engine-agnostic {@link PolicyExpression} model.
7
+ *
8
+ * A `null` clause means "this rule contributes no condition for that clause";
9
+ * consumers apply the default (Postgres denies with `false`).
10
+ */
11
+ export interface RuleConditions {
12
+ usingExpr: PolicyExpression | null;
13
+ withCheckExpr: PolicyExpression | null;
14
+ }
15
+
16
+ /**
17
+ * Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,
18
+ * structured `condition`/`check`, and raw `using`/`withCheck` — into a single
19
+ * normalized {@link PolicyExpression} pair.
20
+ *
21
+ * **This is the linchpin against drift:** both the Postgres DDL generators and
22
+ * the client-side evaluator consume this one function, so there is exactly one
23
+ * definition of what a rule means. In particular, application `roles` are folded
24
+ * into the expression here (AND'd with the base condition, matching how Postgres
25
+ * generates the clause) rather than being handled separately by each consumer.
26
+ */
27
+ export function securityRuleToConditions(rule: SecurityRule): RuleConditions {
28
+ return {
29
+ usingExpr: withRoles(baseUsing(rule), rule),
30
+ withCheckExpr: withRoles(baseWithCheck(rule), rule)
31
+ };
32
+ }
33
+
34
+ function baseUsing(rule: SecurityRule): PolicyExpression | null {
35
+ if (rule.condition) return rule.condition;
36
+ if (rule.using != null) return sqlToPolicy(rule.using);
37
+ if (rule.access === "public") return policy.true();
38
+ if (rule.ownerField) return policy.compare(policy.field(rule.ownerField), "eq", policy.authUid());
39
+ return null;
40
+ }
41
+
42
+ function baseWithCheck(rule: SecurityRule): PolicyExpression | null {
43
+ if (rule.check) return rule.check;
44
+ if (rule.withCheck != null) return sqlToPolicy(rule.withCheck);
45
+ // No explicit WITH CHECK → fall back to the USING condition, matching
46
+ // PostgreSQL's own default behavior.
47
+ return baseUsing(rule);
48
+ }
49
+
50
+ /**
51
+ * AND the base condition with an application-role check, or produce a roles-only
52
+ * condition when there is no base. Mirrors the Postgres generator so that a
53
+ * role-scoped restrictive rule denies exactly the same set of users on both
54
+ * sides.
55
+ */
56
+ function withRoles(base: PolicyExpression | null, rule: SecurityRule): PolicyExpression | null {
57
+ if (!rule.roles || rule.roles.length === 0) return base;
58
+ const rolesExpr = policy.rolesOverlap(rule.roles);
59
+ if (rule.mode === "restrictive") {
60
+ // Restrictive rule: applies ONLY if user has the roles.
61
+ // If user DOES NOT have the roles, they are NOT restricted (passes).
62
+ // If user HAS the roles, they must pass the base condition.
63
+ // Logical equivalent: NOT(roles) OR base
64
+ return base ? policy.or(policy.not(rolesExpr), base) : policy.not(rolesExpr);
65
+ }
66
+ return base ? policy.and(base, rolesExpr) : rolesExpr;
67
+ }
@@ -0,0 +1,88 @@
1
+ import { PolicyExpression, policy } from "@rebasepro/types";
2
+
3
+ /**
4
+ * A tiny, regex-based SQL "parser" for security rules.
5
+ *
6
+ * This is NOT a full SQL parser. It is designed to handle the subset of SQL
7
+ * commonly used in `USING` and `WITH CHECK` clauses, enough to drive the
8
+ * optimistic client-side UI decision.
9
+ *
10
+ * It handles:
11
+ * - `field = 'literal'`
12
+ * - `field != 'literal'`
13
+ * - `field = current_setting('app.user_id')`
14
+ * - `A AND B`
15
+ * - `true`
16
+ * - `IN (...)` (as optimistic true)
17
+ *
18
+ * For anything it doesn't understand, it returns a `raw` expression, which
19
+ * the evaluator treats as "unknown" (and usually optimistic true).
20
+ */
21
+ export function sqlToPolicy(sql: string): PolicyExpression {
22
+ const trimmed = sql.trim();
23
+
24
+ if (trimmed.toLowerCase() === "true") return policy.true();
25
+ if (trimmed.toLowerCase() === "false") return policy.false();
26
+
27
+ // Handle roles overlap (&&)
28
+ // Matches: string_to_array(auth.roles(), ',') && ARRAY['admin', 'editor']
29
+ const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
30
+ if (overlapMatch) {
31
+ const roles = overlapMatch[1].split(",").map(s => s.trim().replace(/^'|'$/g, ""));
32
+ return policy.rolesOverlap(roles);
33
+ }
34
+
35
+ // Handle roles containment (@>)
36
+ // Matches: string_to_array(auth.roles(), ',') @> ARRAY['admin']
37
+ const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
38
+ if (containMatch) {
39
+ const roles = containMatch[1].split(",").map(s => s.trim().replace(/^'|'$/g, ""));
40
+ return policy.rolesContain(roles);
41
+ }
42
+
43
+ // Handle OR
44
+ if (trimmed.toUpperCase().includes(" OR ")) {
45
+ const parts = trimmed.split(/ OR /i);
46
+ return policy.or(...parts.map(sqlToPolicy));
47
+ }
48
+
49
+ // Handle AND (very basic split, doesn't handle nested parens properly)
50
+ if (trimmed.toUpperCase().includes(" AND ")) {
51
+ const parts = trimmed.split(/ AND /i);
52
+ return policy.and(...parts.map(sqlToPolicy));
53
+ }
54
+
55
+ // Handle = and !=
56
+ const match = trimmed.match(/^(.+?)\s*(!?=)\s*(.+)$/);
57
+ if (match) {
58
+ const [, leftStr, op, rightStr] = match;
59
+ const left = parseOperand(leftStr.trim());
60
+ const right = parseOperand(rightStr.trim());
61
+ if (left && right) {
62
+ return policy.compare(left, op === "=" ? "eq" : "neq", right);
63
+ }
64
+ }
65
+
66
+ // Fallback to raw
67
+ return policy.raw(sql);
68
+ }
69
+
70
+ function parseOperand(str: string) {
71
+ // current_setting('app.user_id') or auth.uid()
72
+ if (/current_setting\s*\(\s*'app\.user_id'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) {
73
+ return policy.authUid();
74
+ }
75
+
76
+ // Literal string: 'value'
77
+ const stringMatch = str.match(/^'(.+)'$/);
78
+ if (stringMatch) {
79
+ return policy.literal(stringMatch[1]);
80
+ }
81
+
82
+ // Bare field name
83
+ if (/^\w+$/.test(str)) {
84
+ return policy.field(str);
85
+ }
86
+
87
+ return null;
88
+ }
@@ -1,6 +1,6 @@
1
- import { EntityCollection } from "@rebasepro/types";
1
+ import { CollectionConfig } from "@rebasepro/types";
2
2
 
3
- export function getEntityImagePreviewPropertyKey<M extends Record<string, unknown>>(collection: EntityCollection<M>): string | undefined {
3
+ export function getEntityImagePreviewPropertyKey<M extends Record<string, unknown>>(collection: CollectionConfig<M>): string | undefined {
4
4
 
5
5
  // find first storage property of type image
6
6
  for (const key in collection.properties) {
@@ -26,7 +26,7 @@ export function getEntityImagePreviewPropertyKey<M extends Record<string, unknow
26
26
  // and arrays of URL properties with image preview type
27
27
  for (const key in collection.properties) {
28
28
  const property = collection.properties[key];
29
- if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.url === "image") {
29
+ if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.ui?.url === "image") {
30
30
  return key;
31
31
  }
32
32
  }
@@ -1,18 +1,18 @@
1
- import { CollectionWithRelations, EntityCollection, getDataSourceCapabilities, Property, Relation, RelationProperty } from "@rebasepro/types";
1
+ import { CollectionConfig, getDataSourceCapabilities, Property, Relation, RelationProperty } from "@rebasepro/types";
2
2
  import { toSnakeCase } from "@rebasepro/utils";
3
3
  import { generateForeignKeyName } from "@rebasepro/utils";
4
4
 
5
5
  export function sanitizeRelation(
6
6
  relation: Partial<Relation>,
7
- sourceCollection: EntityCollection,
8
- resolveCollection?: (slugOrTable: string) => EntityCollection | undefined
7
+ sourceCollection: CollectionConfig,
8
+ resolveCollection?: (slugOrTable: string) => CollectionConfig | undefined
9
9
  ): Relation {
10
10
  if (!relation.target) {
11
11
  throw new Error("Relation is missing a `target` collection.");
12
12
  }
13
13
 
14
14
  const rawTarget = relation.target;
15
- let targetCollection: EntityCollection | undefined;
15
+ let targetCollection: CollectionConfig | undefined;
16
16
 
17
17
  if (typeof rawTarget === "string") {
18
18
  if (resolveCollection) {
@@ -20,7 +20,7 @@ export function sanitizeRelation(
20
20
  }
21
21
  if (!targetCollection) {
22
22
  targetCollection = { slug: rawTarget,
23
- name: rawTarget } as EntityCollection;
23
+ name: rawTarget } as CollectionConfig;
24
24
  }
25
25
  } else if (typeof rawTarget === "function") {
26
26
  const evaluated = rawTarget();
@@ -30,13 +30,13 @@ name: rawTarget } as EntityCollection;
30
30
  }
31
31
  if (!targetCollection) {
32
32
  targetCollection = { slug: evaluated,
33
- name: evaluated } as EntityCollection;
33
+ name: evaluated } as CollectionConfig;
34
34
  }
35
35
  } else {
36
36
  targetCollection = evaluated;
37
37
  }
38
38
  } else if (rawTarget && typeof rawTarget === "object") {
39
- targetCollection = rawTarget as EntityCollection;
39
+ targetCollection = rawTarget as CollectionConfig;
40
40
  }
41
41
 
42
42
  if (!targetCollection) {
@@ -89,7 +89,7 @@ name: evaluated } as EntityCollection;
89
89
 
90
90
  try {
91
91
  // Look for an owning relation on the target that points back to this collection
92
- const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? (((targetCollection as CollectionWithRelations).relations) || []) : [];
92
+ const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? (targetCollection.relations || []) : [];
93
93
  for (const targetRel of targetRelations) {
94
94
  if (targetRel.direction === "owning" &&
95
95
  targetRel.cardinality === "one" &&
@@ -135,7 +135,7 @@ name: evaluated } as EntityCollection;
135
135
  // `cardinality: "many" + direction: "owning"` is sufficient to identify owning M2M.
136
136
 
137
137
  // 1. Check the explicit relations[] array
138
- const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? (((targetCollection as CollectionWithRelations).relations) || []) : [];
138
+ const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? (targetCollection.relations || []) : [];
139
139
  for (const targetRel of targetRelations) {
140
140
  if (targetRel.cardinality === "many" &&
141
141
  (targetRel.direction === "owning" || !targetRel.direction) &&
@@ -198,16 +198,15 @@ name: evaluated } as EntityCollection;
198
198
  }
199
199
 
200
200
  /** WeakMap cache — same collection instance always yields the same relation map. */
201
- const _resolvedRelationsCache = new WeakMap<EntityCollection, Record<string, Relation>>();
201
+ const _resolvedRelationsCache = new WeakMap<CollectionConfig, Record<string, Relation>>();
202
202
 
203
203
  export function resolveCollectionRelations(
204
- collection: EntityCollection
204
+ collection: CollectionConfig
205
205
  ): Record<string, Relation> {
206
206
  const cached = _resolvedRelationsCache.get(collection);
207
207
  if (cached) return cached;
208
208
 
209
- if (!getDataSourceCapabilities(collection.driver).supportsRelations) return {};
210
- const relCollection = collection as CollectionWithRelations;
209
+ if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
211
210
  const relations: Record<string, Relation> = {};
212
211
 
213
212
  // Track which explicit relationName values have been registered so that
@@ -217,8 +216,8 @@ export function resolveCollectionRelations(
217
216
 
218
217
  // 1. Process explicit relations from the `relations` field.
219
218
  // Each relation is stored once under its canonical relationName key.
220
- if (relCollection.relations) {
221
- relCollection.relations.forEach((relation: Relation) => {
219
+ if (collection.relations) {
220
+ collection.relations.forEach((relation: Relation) => {
222
221
  try {
223
222
  const normalizedRelation = sanitizeRelation(relation, collection);
224
223
  const relationKey = normalizedRelation.relationName;
@@ -252,7 +251,7 @@ export function resolveCollectionRelations(
252
251
 
253
252
  // We previously skipped if the underlying relation was already registered under
254
253
  // its canonical relationName in section 1. But we need to keep the property mapping
255
- // for EntityFetchService to hydrate the relation back to the correct property key.
254
+ // for FetchService to hydrate the relation back to the correct property key.
256
255
  // Deduplication for Drizzle schema generation is handled in generate-drizzle-schema-logic.ts.
257
256
 
258
257
  if (!relation.relationName) {
@@ -276,7 +275,7 @@ export function resolvePropertyRelation({
276
275
  }: {
277
276
  propertyKey: string;
278
277
  property: Property;
279
- sourceCollection: EntityCollection;
278
+ sourceCollection: CollectionConfig;
280
279
  }): Relation | undefined {
281
280
  if (property.type !== "relation") return undefined;
282
281
 
@@ -305,9 +304,9 @@ export function resolvePropertyRelation({
305
304
  return undefined;
306
305
  }
307
306
 
308
- export function getTableName(collection: EntityCollection): string {
309
- if (getDataSourceCapabilities(collection.driver).supportsRelations) {
310
- return (collection as CollectionWithRelations).table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
307
+ export function getTableName(collection: CollectionConfig): string {
308
+ if (getDataSourceCapabilities(collection.engine).supportsRelations) {
309
+ return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
311
310
  }
312
311
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
313
312
  }
@@ -1,9 +1,7 @@
1
1
  import {
2
2
  ArrayProperty,
3
3
  AuthController,
4
- CollectionWithRelations,
5
- CollectionWithSubcollections,
6
- EntityCollection,
4
+ CollectionConfig,
7
5
  EnumValueConfig,
8
6
  EnumValues,
9
7
  NumberProperty,
@@ -12,7 +10,8 @@ import {
12
10
  Relation,
13
11
  RelationProperty,
14
12
  StringProperty,
15
- getDataSourceCapabilities
13
+ getDataSourceCapabilities,
14
+ getDeclaredSubcollections
16
15
  } from "@rebasepro/types";
17
16
 
18
17
  type PropertyConfig = { property: unknown; [key: string]: unknown };
@@ -342,16 +341,17 @@ export function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefi
342
341
  }
343
342
 
344
343
 
345
- export function getSubcollections<M extends Record<string, unknown> = Record<string, unknown>>(collection: EntityCollection<M>): EntityCollection<Record<string, unknown>>[] {
344
+ export function getSubcollections<M extends Record<string, unknown> = Record<string, unknown>>(collection: CollectionConfig<M>): CollectionConfig<Record<string, unknown>>[] {
346
345
  if (collection.childCollections) {
347
346
  return collection.childCollections() ?? [];
348
347
  }
349
348
 
350
- if (getDataSourceCapabilities(collection.driver).supportsSubcollections && (collection as CollectionWithSubcollections).subcollections) {
351
- return (collection as CollectionWithSubcollections).subcollections!() ?? [];
349
+ const declaredSubcollections = getDeclaredSubcollections(collection);
350
+ if (getDataSourceCapabilities(collection.engine).supportsSubcollections && declaredSubcollections) {
351
+ return declaredSubcollections() ?? [];
352
352
  }
353
353
 
354
- if (getDataSourceCapabilities(collection.driver).supportsRelations) {
354
+ if (getDataSourceCapabilities(collection.engine).supportsRelations) {
355
355
  const resolvedRelations = resolveCollectionRelations(collection);
356
356
  const manyRelations = Object.values(resolvedRelations).filter((r: Relation) => r.cardinality === "many");
357
357
 
@@ -371,7 +371,7 @@ export function getSubcollections<M extends Record<string, unknown> = Record<str
371
371
  }
372
372
  }
373
373
 
374
- const baseOverrides: Partial<EntityCollection> = { slug: relationKey };
374
+ const baseOverrides: Partial<CollectionConfig> = { slug: relationKey };
375
375
  if (customName) {
376
376
  baseOverrides.name = customName;
377
377
  baseOverrides.singularName = customName;
@@ -379,8 +379,8 @@ export function getSubcollections<M extends Record<string, unknown> = Record<str
379
379
 
380
380
  const targetWithOverrides = { ...target,
381
381
  ...baseOverrides };
382
- return (r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides) as EntityCollection<Record<string, unknown>>;
383
- }).filter((c: EntityCollection<Record<string, unknown>> | undefined): c is EntityCollection<Record<string, unknown>> => Boolean(c));
382
+ return (r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides) as CollectionConfig<Record<string, unknown>>;
383
+ }).filter((c: CollectionConfig<Record<string, unknown>> | undefined): c is CollectionConfig<Record<string, unknown>> => Boolean(c));
384
384
  }
385
385
 
386
386
  return [];
@@ -1,6 +1,39 @@
1
- import { ArrayProperty, EntityValues, StorageConfig, StringProperty, UploadedFileContext } from "@rebasepro/types";
1
+ import { ArrayProperty, EntityValues, StorageConfig, StorageSource, StorageSourceRegistry, StringProperty, UploadedFileContext } from "@rebasepro/types";
2
2
  import { randomString } from "@rebasepro/utils";
3
3
 
4
+ /**
5
+ * Resolve the {@link StorageSource} to use for a property, given the key
6
+ * referenced by `StorageConfig.storageSource`.
7
+ *
8
+ * Resolution priority:
9
+ * 1. No `sourceKey` → the default source (backward compatible).
10
+ * 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
11
+ * 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
12
+ * 4. Fall back to the default source.
13
+ *
14
+ * Shared by the upload hook, the markdown editor, and the read-only previews
15
+ * so the resolution logic lives in one place.
16
+ *
17
+ * @group Storage
18
+ */
19
+ export function resolveStorageSource(params: {
20
+ /** Key from `StorageConfig.storageSource`. */
21
+ sourceKey?: string | null;
22
+ /** Built sources keyed by storage-source key (e.g. from context). */
23
+ sources?: Record<string, StorageSource>;
24
+ /** Optional explicit registry — takes precedence over `sources`. */
25
+ registry?: StorageSourceRegistry;
26
+ /** Default source, used when no key is set or the key cannot be resolved. */
27
+ defaultSource: StorageSource;
28
+ }): StorageSource {
29
+ const { sourceKey, sources, registry, defaultSource } = params;
30
+ if (!sourceKey) return defaultSource;
31
+ if (registry) return registry.getOrDefault(sourceKey);
32
+ const fromSources = sources?.[sourceKey];
33
+ if (fromSources) return fromSources;
34
+ return defaultSource;
35
+ }
36
+
4
37
  interface ResolveFilenameStringParams<M extends Record<string, unknown>> {
5
38
  input: string | ((context: UploadedFileContext) => (Promise<string> | string));
6
39
  storage: StorageConfig;