@better-media/core 0.1.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.
@@ -0,0 +1,779 @@
1
+ import { z } from 'zod';
2
+
3
+ type WhereClause = {
4
+ field: string;
5
+ value: unknown;
6
+ operator?: "=" | "!=" | "<" | "<=" | ">" | ">=" | "like" | "in" | "not_in" | "contains" | "starts_with" | "ends_with";
7
+ connector?: "AND" | "OR";
8
+ }[];
9
+ interface CreateOptions<T = Record<string, unknown>> {
10
+ /** Table / collection name */
11
+ model: string;
12
+ /** Data to insert */
13
+ data: T;
14
+ }
15
+ interface FindOptions<T = Record<string, unknown>> {
16
+ /** Table / collection name */
17
+ model: string;
18
+ /** Filter conditions – combined with AND by default */
19
+ where?: WhereClause;
20
+ /** Fields to return; omit to return all */
21
+ select?: (keyof T & string)[];
22
+ /** Sort order */
23
+ sortBy?: {
24
+ field: string;
25
+ direction: "asc" | "desc";
26
+ };
27
+ /** Maximum number of records to return */
28
+ limit?: number;
29
+ /** Number of records to skip */
30
+ offset?: number;
31
+ /** Include soft-deleted records if true */
32
+ withDeleted?: boolean;
33
+ /** Relationships to populate (join/fetch) */
34
+ populate?: string[];
35
+ }
36
+ interface UpdateOptions<T = Record<string, unknown>> {
37
+ /** Table / collection name */
38
+ model: string;
39
+ /** Filter conditions that identify the record(s) to update */
40
+ where: WhereClause;
41
+ /** Partial data to merge into the matched record(s) */
42
+ update: Partial<T>;
43
+ }
44
+ interface DeleteOptions {
45
+ /** Table / collection name */
46
+ model: string;
47
+ /** Filter conditions that identify the record(s) to delete */
48
+ where: WhereClause;
49
+ }
50
+ interface CountOptions {
51
+ /** Table / collection name */
52
+ model: string;
53
+ /** Filter conditions */
54
+ where?: WhereClause;
55
+ }
56
+ /**
57
+ * Adapter specifically for transaction contexts, omitting the ability to nest transactions.
58
+ */
59
+ type DatabaseTransactionAdapter = Omit<DatabaseAdapter, "transaction">;
60
+ /**
61
+ * Engine-agnostic database adapter interface.
62
+ */
63
+ interface DatabaseAdapter {
64
+ /**
65
+ * Insert a new record and return the persisted result (including any
66
+ * server-side defaults such as `id`, `createdAt`, etc.).
67
+ */
68
+ create<T extends Record<string, unknown> = Record<string, unknown>>(options: CreateOptions<T>): Promise<T>;
69
+ /**
70
+ * Return the first record matching `where`, or `null` if no match.
71
+ */
72
+ findOne<T extends Record<string, unknown> = Record<string, unknown>>(options: FindOptions<T>): Promise<T | null>;
73
+ /**
74
+ * Return all records matching `where`. Returns an empty array when there
75
+ * are no matches.
76
+ */
77
+ findMany<T extends Record<string, unknown> = Record<string, unknown>>(options: FindOptions<T>): Promise<T[]>;
78
+ /**
79
+ * Apply a partial update to all records matching `where` and return the
80
+ * first updated record, or `null` if no match was found.
81
+ */
82
+ update<T extends Record<string, unknown> = Record<string, unknown>>(options: UpdateOptions<T>): Promise<T | null>;
83
+ /**
84
+ * Apply a partial update to all records matching `where` and return the
85
+ * number of updated records.
86
+ */
87
+ updateMany<T extends Record<string, unknown> = Record<string, unknown>>(options: UpdateOptions<T>): Promise<number>;
88
+ /**
89
+ * Delete all records matching `where`.
90
+ */
91
+ delete(options: DeleteOptions): Promise<void>;
92
+ /**
93
+ * Delete all records matching `where` and return the number of deleted records.
94
+ */
95
+ deleteMany(options: DeleteOptions): Promise<number>;
96
+ /**
97
+ * Return the number of records matching `where`.
98
+ */
99
+ count(options: CountOptions): Promise<number>;
100
+ /**
101
+ * Execute a raw query. USE WITH CAUTION.
102
+ * This breaks engine-agnosticism.
103
+ */
104
+ raw<T = unknown>(query: string, params?: unknown[]): Promise<T>;
105
+ /**
106
+ * Execute multiple operations within an atomic transaction.
107
+ */
108
+ transaction<R>(callback: (trx: DatabaseTransactionAdapter) => Promise<R>): Promise<R>;
109
+ }
110
+
111
+ /**
112
+ * Job adapter interface for background execution.
113
+ * Implementations: in-memory (default), Redis, RabbitMQ, Kafka, etc.
114
+ */
115
+ interface JobAdapter {
116
+ /** Enqueue a job for background execution */
117
+ enqueue(name: string, payload: Record<string, unknown>): Promise<void>;
118
+ }
119
+
120
+ /** Options for generating a URL to access stored media */
121
+ interface GetUrlOptions {
122
+ /** Expiration time in seconds (for signed URLs). Default depends on adapter. */
123
+ expiresIn?: number;
124
+ }
125
+ /** HTTP method to use for a presigned upload */
126
+ type PresignedUploadMethod = "PUT" | "POST";
127
+ /**
128
+ * Options for creating a presigned upload (PUT or POST).
129
+ * Both methods apply validation as strictly as the S3 protocol allows.
130
+ */
131
+ interface PresignedUploadOptions {
132
+ /**
133
+ * Upload method.
134
+ * - PUT: binary body, signed headers enforce Content-Type + Content-Length.
135
+ * - POST: multipart/form-data with an S3 Policy enforcing size range and Content-Type.
136
+ * Default: "PUT"
137
+ */
138
+ method?: PresignedUploadMethod;
139
+ /** Required MIME type (e.g. "image/jpeg"). Enforced strictly for both methods. */
140
+ contentType: string;
141
+ /** Expiration in seconds. Default: 3600 */
142
+ expiresIn?: number;
143
+ /**
144
+ * Maximum allowed file size in bytes.
145
+ * - POST: enforced by S3 Policy (`content-length-range`). S3 rejects uploads exceeding this.
146
+ * - PUT: encoded as `Content-Length` in the signed command. S3 rejects body size mismatches.
147
+ * For the tightest constraint on PUT, pass the exact expected file size here.
148
+ */
149
+ maxSizeBytes?: number;
150
+ /**
151
+ * Minimum allowed file size in bytes.
152
+ * - POST: enforced by S3 Policy (`content-length-range`). Default: 1.
153
+ * - PUT: not constrainable at the S3 level.
154
+ */
155
+ minSizeBytes?: number;
156
+ /**
157
+ * Additional user-defined metadata to attach to the object (stored as x-amz-meta-* on S3).
158
+ * These are signed into the URL/Policy, so any metadata mismatch causes a 403.
159
+ */
160
+ metadata?: Record<string, string>;
161
+ }
162
+ /**
163
+ * Unified presigned upload result — same shape for both PUT and POST.
164
+ *
165
+ * @example PUT usage (mobile app / API client):
166
+ * ```ts
167
+ * fetch(result.url, {
168
+ * method: "PUT",
169
+ * headers: result.headers,
170
+ * body: fileBlob,
171
+ * });
172
+ * ```
173
+ *
174
+ * @example POST usage (browser form / web app):
175
+ * ```ts
176
+ * const form = new FormData();
177
+ * for (const [key, value] of Object.entries(result.fields ?? {})) {
178
+ * form.append(key, value);
179
+ * }
180
+ * form.append("file", fileBlob); // file field MUST be last
181
+ * fetch(result.url, { method: "POST", body: form });
182
+ * ```
183
+ */
184
+ interface PresignedUploadResult {
185
+ method: PresignedUploadMethod;
186
+ /** The URL to upload to. For POST this is the bucket endpoint; for PUT it is the fully signed URL. */
187
+ url: string;
188
+ /**
189
+ * Required form fields (POST only).
190
+ * Must be appended to the multipart/form-data body BEFORE the file field, in the order provided.
191
+ */
192
+ fields?: Record<string, string>;
193
+ /**
194
+ * Required HTTP headers (PUT only).
195
+ * The client MUST send all of these headers exactly as specified.
196
+ * Mismatch causes S3 to return 403 Forbidden.
197
+ */
198
+ headers?: Record<string, string>;
199
+ }
200
+ interface StorageAdapter {
201
+ get(key: string): Promise<Buffer | null>;
202
+ put(key: string, value: Buffer): Promise<void>;
203
+ delete(key: string): Promise<void>;
204
+ /**
205
+ * Check if a key exists without loading the full content.
206
+ */
207
+ exists(key: string): Promise<boolean>;
208
+ /**
209
+ * Optional: get file size in bytes without loading the full buffer.
210
+ * Used with fileHandling.maxBufferBytes to decide buffer vs stream.
211
+ */
212
+ getSize?(key: string): Promise<number | null>;
213
+ /**
214
+ * Optional: stream file contents. Used when file exceeds maxBufferBytes.
215
+ */
216
+ getStream?(key: string): Promise<ReadableStream | null>;
217
+ /**
218
+ * Generate a signed or public URL to access the file.
219
+ * Optional: S3/GCS adapters implement this; memory adapter does not.
220
+ */
221
+ getUrl?(key: string, options?: GetUrlOptions): Promise<string>;
222
+ /**
223
+ * Create a presigned upload for direct-to-storage upload.
224
+ * Supports both PUT (binary body) and POST (multipart form) with strict validation.
225
+ * Optional: S3/GCS adapters implement this; memory adapter does not.
226
+ */
227
+ createPresignedUpload?(key: string, options: PresignedUploadOptions): Promise<PresignedUploadResult>;
228
+ /**
229
+ * Remove all stored keys. Optional; mainly for testing/dev (e.g. memory adapter).
230
+ */
231
+ clear?(): Promise<void>;
232
+ }
233
+
234
+ /** Core file information. Populated by upload adapter / upload:init. */
235
+ interface FileInfo {
236
+ /** Logical key in storage. Always present. */
237
+ key: string;
238
+ /** File size in bytes */
239
+ size?: number;
240
+ /** MIME type (e.g. "image/jpeg"). Use this instead of contentType/mimeType. */
241
+ mimeType?: string;
242
+ /** Original filename from upload (e.g. "photo.jpg") */
243
+ originalName?: string;
244
+ /** File extension (e.g. ".jpg"). Derived from key or originalName. */
245
+ extension?: string;
246
+ /** Checksum for validation (e.g. sha256). Key names are hashing algorithm. */
247
+ checksums?: Record<string, string>;
248
+ }
249
+
250
+ /** Where the file lives in storage. Populated by upload adapter or upload:init. */
251
+ interface StorageLocation {
252
+ /** Logical key (same as file.key). Always present. */
253
+ key: string;
254
+ /** Bucket name (S3, GCS). Optional for memory/local adapters. */
255
+ bucket?: string;
256
+ /** Region (S3). Optional. */
257
+ region?: string;
258
+ /** Public or pre-signed URL. Optional; computed on demand by adapters. */
259
+ url?: string;
260
+ }
261
+
262
+ /** A single thumbnail or derived image */
263
+ interface ThumbnailResult {
264
+ key: string;
265
+ width?: number;
266
+ height?: number;
267
+ format?: string;
268
+ url?: string;
269
+ }
270
+ /** A transcoded or converted variant */
271
+ interface VariantResult {
272
+ key: string;
273
+ format?: string;
274
+ width?: number;
275
+ height?: number;
276
+ bitrate?: number;
277
+ url?: string;
278
+ }
279
+ /** Image/video dimensions. Set by validation or processing. */
280
+ interface MediaDimensions {
281
+ width: number;
282
+ height: number;
283
+ /** Duration in seconds, for video */
284
+ duration?: number;
285
+ }
286
+ /** Output from processing plugins. Each plugin writes to a namespaced key to avoid overwrites. */
287
+ interface ProcessingResults {
288
+ /** Dimensions (from validation or processing) */
289
+ dimensions?: MediaDimensions;
290
+ /** Thumbnails. Plugins append; use plugin name as key if multiple sources. */
291
+ thumbnails?: Record<string, ThumbnailResult[]>;
292
+ /** Variants (transcodes, conversions) */
293
+ variants?: Record<string, VariantResult[]>;
294
+ /** Plugin-specific results. Key by plugin name. */
295
+ [pluginKey: string]: unknown;
296
+ }
297
+
298
+ /**
299
+ * Zod Schema for Trusted Metadata.
300
+ * Strictly enforced to prevent trust injection from the database.
301
+ */
302
+ declare const TrustedMetadataSchema: z.ZodObject<{
303
+ file: z.ZodOptional<z.ZodObject<{
304
+ mimeType: z.ZodOptional<z.ZodString>;
305
+ size: z.ZodOptional<z.ZodNumber>;
306
+ originalName: z.ZodOptional<z.ZodString>;
307
+ extension: z.ZodOptional<z.ZodString>;
308
+ }, "strict", z.ZodTypeAny, {
309
+ mimeType?: string | undefined;
310
+ size?: number | undefined;
311
+ originalName?: string | undefined;
312
+ extension?: string | undefined;
313
+ }, {
314
+ mimeType?: string | undefined;
315
+ size?: number | undefined;
316
+ originalName?: string | undefined;
317
+ extension?: string | undefined;
318
+ }>>;
319
+ checksums: z.ZodOptional<z.ZodObject<{
320
+ sha256: z.ZodOptional<z.ZodString>;
321
+ md5: z.ZodOptional<z.ZodString>;
322
+ }, "strict", z.ZodTypeAny, {
323
+ sha256?: string | undefined;
324
+ md5?: string | undefined;
325
+ }, {
326
+ sha256?: string | undefined;
327
+ md5?: string | undefined;
328
+ }>>;
329
+ media: z.ZodOptional<z.ZodObject<{
330
+ width: z.ZodOptional<z.ZodNumber>;
331
+ height: z.ZodOptional<z.ZodNumber>;
332
+ duration: z.ZodOptional<z.ZodNumber>;
333
+ }, "strict", z.ZodTypeAny, {
334
+ width?: number | undefined;
335
+ height?: number | undefined;
336
+ duration?: number | undefined;
337
+ }, {
338
+ width?: number | undefined;
339
+ height?: number | undefined;
340
+ duration?: number | undefined;
341
+ }>>;
342
+ }, "strict", z.ZodTypeAny, {
343
+ file?: {
344
+ mimeType?: string | undefined;
345
+ size?: number | undefined;
346
+ originalName?: string | undefined;
347
+ extension?: string | undefined;
348
+ } | undefined;
349
+ checksums?: {
350
+ sha256?: string | undefined;
351
+ md5?: string | undefined;
352
+ } | undefined;
353
+ media?: {
354
+ width?: number | undefined;
355
+ height?: number | undefined;
356
+ duration?: number | undefined;
357
+ } | undefined;
358
+ }, {
359
+ file?: {
360
+ mimeType?: string | undefined;
361
+ size?: number | undefined;
362
+ originalName?: string | undefined;
363
+ extension?: string | undefined;
364
+ } | undefined;
365
+ checksums?: {
366
+ sha256?: string | undefined;
367
+ md5?: string | undefined;
368
+ } | undefined;
369
+ media?: {
370
+ width?: number | undefined;
371
+ height?: number | undefined;
372
+ duration?: number | undefined;
373
+ } | undefined;
374
+ }>;
375
+ /** Metadata from DB/Trusted Sources (Schema validated) */
376
+ type TrustedMetadata = z.infer<typeof TrustedMetadataSchema>;
377
+
378
+ /** File content loaded by framework. buffer or tempPath (when streamed to disk). */
379
+ interface FileContent {
380
+ buffer?: Buffer;
381
+ tempPath?: string;
382
+ }
383
+ interface PipelineContext {
384
+ /** The unique database record identifier (typically a generated UUID). */
385
+ recordId: string;
386
+ /** Core file information. Mutable. */
387
+ file: FileInfo;
388
+ /** Storage location. Mutable but typically set once. */
389
+ storageLocation: StorageLocation;
390
+ /** Processing outputs (thumbnails, variants, etc). Mutable. */
391
+ processing: ProcessingResults;
392
+ /** Custom app/plugin metadata. Mutable. */
393
+ metadata: Record<string, unknown>;
394
+ /**
395
+ * Plugin-derived metadata. First writer wins.
396
+ * Prefilled from DB when available. Plugins read when set, compute and write when missing.
397
+ */
398
+ trusted: TrustedMetadata;
399
+ /** Storage adapter – read-only reference */
400
+ storage: StorageAdapter;
401
+ /** Database adapter – read-only reference */
402
+ database: DatabaseAdapter;
403
+ /** Job adapter – read-only reference */
404
+ jobs: JobAdapter;
405
+ /** Plugin scratchpad. Not persisted. Mutable. Includes file content (buffer/tempPath) from framework. */
406
+ utilities?: Record<string, unknown> & {
407
+ fileContent?: FileContent;
408
+ };
409
+ }
410
+ /** Framework-mediated Write API for Plugins */
411
+ interface PluginApi {
412
+ /** Emit metadata under the plugin's own namespace. Scoped automatically. */
413
+ emitMetadata(patch: Record<string, unknown>): void;
414
+ /** Emit processing results (transcription, thumbnails) under plugin's namespace. */
415
+ emitProcessing(patch: Record<string, unknown>): void;
416
+ /** Propose updates to global trusted metadata. ONLY for 'trusted' plugins. */
417
+ proposeTrusted(patch: TrustedMetadata): void;
418
+ }
419
+
420
+ /** Lifecycle hook names (aligns with AWS/Cloudinary media pipeline stages). Single source of truth. */
421
+ declare const HOOK_NAMES: readonly ["upload:init", "validation:run", "scan:run", "process:run", "upload:complete"];
422
+ type HookName = (typeof HOOK_NAMES)[number];
423
+
424
+ /**
425
+ * Result from validation-phase handlers; can abort pipeline.
426
+ * Persisted to 'validation_results' table.
427
+ */
428
+ declare const ValidationResultSchema: z.ZodObject<{
429
+ valid: z.ZodBoolean;
430
+ pluginId: z.ZodOptional<z.ZodString>;
431
+ message: z.ZodOptional<z.ZodString>;
432
+ errors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
433
+ }, "strict", z.ZodTypeAny, {
434
+ valid: boolean;
435
+ message?: string | undefined;
436
+ pluginId?: string | undefined;
437
+ errors?: string[] | undefined;
438
+ }, {
439
+ valid: boolean;
440
+ message?: string | undefined;
441
+ pluginId?: string | undefined;
442
+ errors?: string[] | undefined;
443
+ }>;
444
+ type ValidationResult = z.infer<typeof ValidationResultSchema>;
445
+ /**
446
+ * Result from virus-scan handlers.
447
+ * Persisted to 'virus_scan_results' table.
448
+ */
449
+ declare const VirusScanResultSchema: z.ZodObject<{
450
+ status: z.ZodEnum<["clean", "infected", "error"]>;
451
+ threats: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
452
+ scanner: z.ZodOptional<z.ZodString>;
453
+ scannedAt: z.ZodOptional<z.ZodString>;
454
+ }, "strict", z.ZodTypeAny, {
455
+ status: "clean" | "infected" | "error";
456
+ threats?: string[] | undefined;
457
+ scanner?: string | undefined;
458
+ scannedAt?: string | undefined;
459
+ }, {
460
+ status: "clean" | "infected" | "error";
461
+ threats?: string[] | undefined;
462
+ scanner?: string | undefined;
463
+ scannedAt?: string | undefined;
464
+ }>;
465
+ /** Hook interface for plugin registration (Tapable-style) */
466
+ interface MediaRuntimeHook {
467
+ tap(name: string, fn: (ctx: PipelineContext, api: PluginApi) => Promise<void | ValidationResult>, options?: {
468
+ mode?: "sync" | "background";
469
+ }): void;
470
+ }
471
+ /** Runtime host passed to plugins via apply(); exposes lifecycle hooks */
472
+ interface MediaRuntime {
473
+ readonly hooks: {
474
+ [K in HookName]: MediaRuntimeHook;
475
+ };
476
+ }
477
+
478
+ /** Plugin execution mode: sync (inline) or background (queued) */
479
+ type PluginExecutionMode = "sync" | "background";
480
+ /**
481
+ * Plugin capabilities: what a plugin is allowed to do.
482
+ */
483
+ type PluginCapability = "file.read" | "metadata.write.own" | "processing.write.own" | "trusted.propose";
484
+ /**
485
+ * Plugin trust level: untrusted (default) or trusted.
486
+ */
487
+ type PluginTrustLevel = "untrusted" | "trusted";
488
+ /**
489
+ * Plugin Manifest: immutable identity and requirements.
490
+ */
491
+ interface PluginManifest {
492
+ readonly id: string;
493
+ readonly version: string;
494
+ readonly trustLevel: PluginTrustLevel;
495
+ readonly capabilities: PluginCapability[];
496
+ readonly namespace: string;
497
+ }
498
+ /**
499
+ * Pipeline Plugin (apply pattern, Webpack/Babel style)
500
+ * - apply: receive runtime, tap into hooks (preferred)
501
+ * - execute: legacy single-phase; mapped to process:run when apply is absent
502
+ */
503
+ interface PipelinePlugin {
504
+ readonly name: string;
505
+ /** Manifest: Required identity and security metadata */
506
+ readonly runtimeManifest: PluginManifest;
507
+ /** Apply: receive runtime, tap into hooks. Standard Webpack/Babel pattern. */
508
+ apply?(runtime: MediaRuntime): void;
509
+ /** Legacy: single-phase execution. Mapped to process:run when apply is absent. */
510
+ execute?(context: PipelineContext): Promise<void>;
511
+ /** Execution mode: sync (default) or background */
512
+ readonly executionMode?: PluginExecutionMode;
513
+ /** Intensive plugins run in background by default. Non-intensive default to sync. */
514
+ readonly intensive?: boolean;
515
+ }
516
+
517
+ /** Provenance tokens for trusted metadata (e.g. proposeTrusted). */
518
+ type VerifiedSourceId = "file:content";
519
+ interface PipelineContextWithVerified extends PipelineContext {
520
+ _verifiedSources?: Set<VerifiedSourceId>;
521
+ }
522
+ /**
523
+ * Mark that this pipeline run has read file bytes from storage (buffer or temp path).
524
+ * Required before trusted plugins may call proposeTrusted with file/checksums/media patches.
525
+ */
526
+ declare function markFileContentVerified(context: PipelineContext): void;
527
+
528
+ /** Plugin lifecycle hooks (extensible) */
529
+ interface PluginHooks {
530
+ /** Hook names → handlers. Structure for future use. */
531
+ [key: string]: (...args: unknown[]) => Promise<void>;
532
+ }
533
+
534
+ /**
535
+ * Hook-level execution mode constraint.
536
+ * - sync-only: Handler always runs inline. Passing background is overridden with a warning.
537
+ * - sync-or-background: Plugin may choose either; both are valid.
538
+ * - background-only: Handler always runs enqueued. Passing sync is overridden with a warning.
539
+ */
540
+ type HookModeConstraint = "sync-only" | "sync-or-background" | "background-only";
541
+ /** Per-hook mode constraints. Single source of truth for hook execution rules. */
542
+ declare const HOOK_MODE_CONSTRAINTS: Record<HookName, HookModeConstraint>;
543
+ /**
544
+ * Resolve effective execution mode given hook constraint and requested mode.
545
+ * Returns the mode to use and whether it was overridden (for logging).
546
+ */
547
+ declare function resolveHookMode(hookName: HookName, requested: "sync" | "background"): {
548
+ effective: "sync" | "background";
549
+ overridden: boolean;
550
+ };
551
+
552
+ /**
553
+ * Handler for a pipeline stage hook.
554
+ * Can return ValidationResult to abort pipeline (validation phases).
555
+ */
556
+ type HookHandler$1 = (ctx: PipelineContext) => Promise<void | ValidationResult>;
557
+ /** Options for hook registration (tap) */
558
+ interface HookHandlerOptions {
559
+ /** Sync (inline) or background (queued via JobAdapter) */
560
+ mode?: "sync" | "background";
561
+ }
562
+
563
+ /** Plugin contract alias – plugins register themselves via apply() and tap into hooks */
564
+ type MediaPlugin = PipelinePlugin;
565
+ /** Context passed to hook handlers – pipeline stage execution context */
566
+ type HookContext = PipelineContext;
567
+
568
+ type FieldType = "string" | "number" | "boolean" | "date" | "json";
569
+ interface FieldDefinition {
570
+ type: FieldType;
571
+ /** Whether this field is the primary key */
572
+ primaryKey?: boolean;
573
+ /** Whether this field must always have a value */
574
+ required?: boolean;
575
+ /** Whether the values in this field must be unique across the table */
576
+ unique?: boolean;
577
+ /** Default value for new records */
578
+ defaultValue?: unknown | (() => unknown);
579
+ /** Foreign key reference */
580
+ references?: {
581
+ model: string;
582
+ field: string;
583
+ onDelete?: "cascade" | "set null" | "restrict";
584
+ };
585
+ }
586
+ interface IndexDefinition {
587
+ fields: string[];
588
+ unique?: boolean;
589
+ }
590
+ interface ModelDefinition {
591
+ fields: Record<string, FieldDefinition>;
592
+ indexes?: IndexDefinition[];
593
+ /** Whether to enable soft delete for this model */
594
+ softDelete?: boolean;
595
+ }
596
+ type BmSchema = Record<string, ModelDefinition>;
597
+ /**
598
+ * Context passed to database hooks.
599
+ */
600
+ interface DatabaseHookContext {
601
+ model: string;
602
+ adapter: DatabaseAdapter;
603
+ transaction?: DatabaseTransactionAdapter;
604
+ }
605
+ type HookHandler<T = unknown, R = unknown> = (data: T, context: DatabaseHookContext) => Promise<R>;
606
+ interface DbHooks {
607
+ before?: {
608
+ create?: HookHandler<Record<string, unknown>, Record<string, unknown>>[];
609
+ update?: HookHandler<Record<string, unknown>, Record<string, unknown>>[];
610
+ delete?: HookHandler<WhereClause, void>[];
611
+ };
612
+ after?: {
613
+ create?: HookHandler<Record<string, unknown>, void>[];
614
+ update?: HookHandler<Record<string, unknown>, void>[];
615
+ delete?: HookHandler<WhereClause, void>[];
616
+ };
617
+ }
618
+ type SqlDialect = "postgres" | "mysql" | "sqlite" | "mssql";
619
+ interface ColumnMetadata {
620
+ name: string;
621
+ dataType: string;
622
+ isNullable: boolean;
623
+ isUnique?: boolean;
624
+ }
625
+ interface TableMetadata {
626
+ name: string;
627
+ columns: ColumnMetadata[];
628
+ }
629
+ type MigrationOperation = {
630
+ type: "createTable";
631
+ table: string;
632
+ definition: ModelDefinition;
633
+ } | {
634
+ type: "addColumn";
635
+ table: string;
636
+ field: string;
637
+ definition: FieldDefinition;
638
+ } | {
639
+ type: "createIndex";
640
+ table: string;
641
+ name: string;
642
+ fields: string[];
643
+ unique?: boolean;
644
+ };
645
+
646
+ /**
647
+ * Central schema defining all Better Media tables and relationships.
648
+ * This is the single source of truth for the database structure.
649
+ */
650
+ declare const schema: BmSchema;
651
+
652
+ declare function toSnakeCase(value: string): string;
653
+ declare function toCamelCase(value: string): string;
654
+ declare function toDbFieldName(field: string): string;
655
+
656
+ interface FieldConverter {
657
+ serialize(value: unknown): unknown;
658
+ deserialize(value: unknown): unknown;
659
+ }
660
+ declare const converters: Record<FieldType, FieldConverter>;
661
+ declare function serializeField(type: FieldType, value: unknown): unknown;
662
+ declare function deserializeField(type: FieldType, value: unknown): unknown;
663
+ declare function serializeData(fields: Record<string, {
664
+ type: FieldType;
665
+ }>, data: Record<string, unknown>): Record<string, unknown>;
666
+ declare function deserializeData(fields: Record<string, {
667
+ type: FieldType;
668
+ }>, data: Record<string, unknown>): Record<string, unknown>;
669
+
670
+ declare function getColumnType(field: FieldDefinition, dialect: SqlDialect): string;
671
+ declare function matchType(dbType: string, expectedType: FieldType, dialect: SqlDialect): boolean;
672
+ /**
673
+ * MigrationPlanner calculates the delta between the desired schema and the actual DB state.
674
+ */
675
+ declare class MigrationPlanner {
676
+ private readonly dialect;
677
+ constructor(dialect: SqlDialect);
678
+ plan(schema: BmSchema, currentTables: TableMetadata[]): MigrationOperation[];
679
+ }
680
+ /**
681
+ * Applies a list of migration operations to an existing metadata state to project
682
+ * what the database will look like after the migration.
683
+ */
684
+ declare function applyOperationsToMetadata(currentMetadata: TableMetadata[], operations: MigrationOperation[], dialect: SqlDialect): TableMetadata[];
685
+
686
+ declare function compileMigrationOperationsSql(options: {
687
+ operations: MigrationOperation[];
688
+ dialect: SqlDialect;
689
+ }): string;
690
+ declare function generateCreateSchemaSql(options: {
691
+ schema: BmSchema;
692
+ dialect: SqlDialect;
693
+ }): string;
694
+
695
+ interface MigrationOptions {
696
+ /**
697
+ * Migration mode:
698
+ * - 'safe': Only create missing tables/collections and add missing columns/indexes.
699
+ * - 'diff': Full schema comparison and incremental updates (Recommended).
700
+ * - 'force': Drop and recreate all tables/collections (destructive).
701
+ */
702
+ mode?: "safe" | "diff" | "force";
703
+ /**
704
+ * For SQL adapters, the dialect can be manually specified.
705
+ * If omitted, the adapter will try to detect it.
706
+ */
707
+ dialect?: SqlDialect;
708
+ }
709
+ interface PlannedMigrationTable {
710
+ table: string;
711
+ fields: string[];
712
+ }
713
+ interface PlannedMigrations {
714
+ toBeCreated: PlannedMigrationTable[];
715
+ toBeAdded: PlannedMigrationTable[];
716
+ operations: MigrationOperation[];
717
+ compileMigrations: () => string;
718
+ runMigrations: () => Promise<void>;
719
+ }
720
+ declare function getMigrations(adapter: DatabaseAdapter, options?: MigrationOptions): Promise<PlannedMigrations>;
721
+ /**
722
+ * Migration engine. It iterates through the central BmSchema and invokes
723
+ * engine-specific setup commands on the adapter.
724
+ */
725
+ declare function runMigrations(adapter: DatabaseAdapter, options?: MigrationOptions): Promise<void>;
726
+
727
+ /**
728
+ * Utility to run hooks sequentially.
729
+ * Supports multiple handlers and passes DatabaseHookContext.
730
+ */
731
+ declare const runHooks: {
732
+ beforeCreate(hooks: DbHooks | undefined, data: Record<string, unknown>, context: DatabaseHookContext): Promise<Record<string, unknown>>;
733
+ beforeUpdate(hooks: DbHooks | undefined, data: Record<string, unknown>, context: DatabaseHookContext): Promise<Record<string, unknown>>;
734
+ beforeDelete(hooks: DbHooks | undefined, where: WhereClause, context: DatabaseHookContext): Promise<void>;
735
+ afterCreate(hooks: DbHooks | undefined, result: Record<string, unknown>, context: DatabaseHookContext): Promise<void>;
736
+ afterUpdate(hooks: DbHooks | undefined, result: Record<string, unknown>, context: DatabaseHookContext): Promise<void>;
737
+ afterDelete(hooks: DbHooks | undefined, where: WhereClause, context: DatabaseHookContext): Promise<void>;
738
+ };
739
+
740
+ type QueryResultLike<T = unknown> = {
741
+ rows: T[];
742
+ rowCount?: number | null;
743
+ };
744
+ type Queryable = {
745
+ query: (text: string, values?: unknown[]) => Promise<QueryResultLike<Record<string, unknown>>>;
746
+ };
747
+ type PgPoolLike = Queryable & {
748
+ connect?: () => Promise<PgClientLike>;
749
+ };
750
+ type PgClientLike = Queryable & {
751
+ release?: () => void;
752
+ };
753
+ declare function isPgPoolLike(value: unknown): value is PgPoolLike;
754
+ /**
755
+ * Normalizes a database pool or adapter into a standard DatabaseAdapter.
756
+ * Note: If using the built-in Postgres pool, this requires the actual
757
+ * implementation to be registered or known.
758
+ */
759
+ declare function toDatabaseAdapter(database: DatabaseAdapter | PgPoolLike, postgresFactory?: (pool: PgPoolLike) => DatabaseAdapter): DatabaseAdapter;
760
+
761
+ type GetAdapterOptions = {
762
+ database?: DatabaseAdapter | PgPoolLike;
763
+ createDatabase?: () => Promise<DatabaseAdapter | PgPoolLike> | DatabaseAdapter | PgPoolLike;
764
+ dialect?: string;
765
+ schemaOutput?: string;
766
+ migrationsDir?: string;
767
+ };
768
+ declare function getAdapter(options: GetAdapterOptions): Promise<DatabaseAdapter>;
769
+
770
+ /**
771
+ * Mapping of common file extensions to their valid MIME types.
772
+ * Based on MDN Common Media Types: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types
773
+ */
774
+ declare const EXTENSION_TO_MIME_MAP: Record<string, string[]>;
775
+
776
+ type MediaStatus = "PENDING_VERIFICATION" | "VALID" | "INVALID" | "PROCESSING" | "COMPLETED";
777
+ declare function sleep(ms: number): Promise<unknown>;
778
+
779
+ export { type BmSchema, type ColumnMetadata, type CountOptions, type CreateOptions, type DatabaseAdapter, type DatabaseHookContext, type DatabaseTransactionAdapter, type DbHooks, type DeleteOptions, EXTENSION_TO_MIME_MAP, type FieldConverter, type FieldDefinition, type FieldType, type FileContent, type FileInfo, type FindOptions, type GetAdapterOptions, type GetUrlOptions, HOOK_MODE_CONSTRAINTS, HOOK_NAMES, type HookContext, type HookHandler$1 as HookHandler, type HookHandlerOptions, type HookModeConstraint, type HookName, type IndexDefinition, type JobAdapter, type MediaDimensions, type MediaPlugin, type MediaRuntime, type MediaRuntimeHook, type MediaStatus, type MigrationOperation, type MigrationOptions, MigrationPlanner, type ModelDefinition, type PgClientLike, type PgPoolLike, type PipelineContext, type PipelineContextWithVerified, type PipelinePlugin, type PlannedMigrationTable, type PlannedMigrations, type PluginApi, type PluginExecutionMode, type PluginHooks, type PluginManifest, type PresignedUploadMethod, type PresignedUploadOptions, type PresignedUploadResult, type ProcessingResults, type QueryResultLike, type Queryable, type SqlDialect, type StorageAdapter, type StorageLocation, type TableMetadata, type ThumbnailResult, type TrustedMetadata, TrustedMetadataSchema, type UpdateOptions, type ValidationResult, ValidationResultSchema, type VariantResult, type VerifiedSourceId, VirusScanResultSchema, type WhereClause, applyOperationsToMetadata, compileMigrationOperationsSql, converters, deserializeData, deserializeField, generateCreateSchemaSql, getAdapter, getColumnType, getMigrations, isPgPoolLike, markFileContentVerified, matchType, resolveHookMode, runHooks, runMigrations, schema, serializeData, serializeField, sleep, toCamelCase, toDatabaseAdapter, toDbFieldName, toSnakeCase };