@opensaas/stack-storage 0.23.0 → 0.25.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @opensaas/stack-storage@0.23.0 build /home/runner/work/stack/stack/packages/storage
2
+ > @opensaas/stack-storage@0.25.0 build /home/runner/work/stack/stack/packages/storage
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @opensaas/stack-storage
2
2
 
3
+ ## 0.25.0
4
+
5
+ ## 0.24.0
6
+
7
+ ### Minor Changes
8
+
9
+ - [#551](https://github.com/OpenSaasAU/stack/pull/551) [`fb979b8`](https://github.com/OpenSaasAU/stack/commit/fb979b8978f9bdefd2b4f81e87c1c198582200ae) Thanks [@borisno2](https://github.com/borisno2)! - Add a storage provider registration API so non-`local` and custom providers are constructable.
10
+
11
+ `createStorageProvider` now resolves a provider `type` through a registry instead of a hardcoded `switch`, which previously only built `'local'` and threw for everything else. `'local'` is registered as a built-in default, so existing behaviour is unchanged. The host opts into the optional provider packages (`@opensaas/stack-storage-s3`, `@opensaas/stack-storage-vercel`) or a custom provider by registering it — `@opensaas/stack-storage` does not depend on the provider packages, keeping the AWS/Vercel SDKs off every storage user. Reads are unaffected: assembling existing asset metadata only stamps the provider name and never constructs a provider.
12
+
13
+ ```typescript
14
+ // lib/register-storage.ts (server-only, imported at app startup)
15
+ import { registerStorageProvider } from '@opensaas/stack-storage/runtime'
16
+ import { S3StorageProvider, type S3StorageConfig } from '@opensaas/stack-storage-s3'
17
+
18
+ registerStorageProvider<S3StorageConfig>('s3', (config) => new S3StorageProvider(config))
19
+ ```
20
+
21
+ ```typescript
22
+ // opensaas.config.ts — reference the registered provider by type
23
+ import { s3Storage } from '@opensaas/stack-storage-s3'
24
+
25
+ export default config({
26
+ storage: {
27
+ avatars: s3Storage({ bucket: 'user-avatars', region: 'us-east-1' }),
28
+ },
29
+ // ...
30
+ })
31
+ ```
32
+
33
+ Custom providers register the same way: implement `StorageProvider`, give it a `type`, then call `registerStorageProvider(type, (config) => new MyProvider(config))`. An unregistered type throws a clear error pointing at `registerStorageProvider`.
34
+
3
35
  ## 0.23.0
4
36
 
5
37
  ## 0.22.0
package/CLAUDE.md CHANGED
@@ -50,7 +50,10 @@ packages/storage-vercel/ # Separate Vercel Blob provider package
50
50
  - `uploadImage(config, provider, data, options)` - Upload image with transformations
51
51
  - `deleteFile(config, provider, filename)` - Delete file
52
52
  - `deleteImage(config, metadata)` - Delete image and all transformations
53
- - `createStorageProvider(config, providerName)` - Create provider instance
53
+ - `createStorageProvider(config, providerName)` - Create provider instance (resolves the provider `type` through the registry)
54
+ - `registerStorageProvider(type, factory)` - Register a provider `type` → constructor (host opts in to optional/custom providers)
55
+ - `getStorageProviderFactory(type)` / `hasStorageProvider(type)` - Inspect the registry
56
+ - `resetStorageProviderRegistry()` - Reset to built-in defaults (mainly for tests)
54
57
 
55
58
  ### Utils (`src/utils/`)
56
59
 
@@ -352,40 +355,139 @@ avatar: image({
352
355
  })
353
356
  ```
354
357
 
355
- ### Custom Storage Provider
358
+ ### Storage Provider Registry
359
+
360
+ `createStorageProvider` resolves a provider `type` through a **registry** rather
361
+ than a hardcoded `switch`. `'local'` is registered as a built-in default, so it
362
+ works with no setup. Every other provider — the optional packages
363
+ (`@opensaas/stack-storage-s3`, `@opensaas/stack-storage-vercel`) and your own
364
+ custom providers — must be **registered by the host** before
365
+ `createStorageProvider` can build them.
366
+
367
+ `@opensaas/stack-storage` deliberately does **not** depend on `-s3`/`-vercel`:
368
+ wiring them into the factory directly would force the AWS/Vercel SDKs onto every
369
+ storage user. The host opts in by registering only the provider(s) it uses.
370
+
371
+ #### Registering an optional provider package
372
+
373
+ Register once at app startup (e.g. in a server-only module imported by your app
374
+ entry, the upload route, or `instrumentation.ts`):
375
+
376
+ ```typescript
377
+ // lib/register-storage.ts (server-only)
378
+ import { registerStorageProvider } from '@opensaas/stack-storage/runtime'
379
+ import { S3StorageProvider, type S3StorageConfig } from '@opensaas/stack-storage-s3'
380
+
381
+ registerStorageProvider<S3StorageConfig>('s3', (config) => new S3StorageProvider(config))
382
+ ```
383
+
384
+ Then reference the provider by `type` in config (via the package's config
385
+ builder, which sets `type: 's3'`):
386
+
387
+ ```typescript
388
+ import { s3Storage } from '@opensaas/stack-storage-s3'
389
+
390
+ export default config({
391
+ storage: {
392
+ avatars: s3Storage({ bucket: 'user-avatars', region: 'us-east-1' }),
393
+ },
394
+ // ...
395
+ })
396
+ ```
397
+
398
+ The Vercel Blob provider follows the same shape:
399
+
400
+ ```typescript
401
+ import { registerStorageProvider } from '@opensaas/stack-storage/runtime'
402
+ import {
403
+ VercelBlobStorageProvider,
404
+ type VercelBlobStorageConfig,
405
+ } from '@opensaas/stack-storage-vercel'
406
+
407
+ registerStorageProvider<VercelBlobStorageConfig>(
408
+ 'vercel-blob',
409
+ (config) => new VercelBlobStorageProvider(config),
410
+ )
411
+ ```
412
+
413
+ #### Registering a custom provider
414
+
415
+ Implement `StorageProvider`, give it a `type` discriminator, then register it:
356
416
 
357
417
  ```typescript
358
418
  // lib/cloudflare-r2-storage.ts
359
- import type { StorageProvider } from '@opensaas/stack-storage'
419
+ import type {
420
+ StorageProvider,
421
+ UploadOptions,
422
+ UploadResult,
423
+ BaseStorageConfig,
424
+ } from '@opensaas/stack-storage'
425
+
426
+ export interface CloudflareR2Config extends BaseStorageConfig {
427
+ type: 'cloudflare-r2'
428
+ bucket: string
429
+ accountId: string
430
+ }
360
431
 
361
432
  export class CloudflareR2StorageProvider implements StorageProvider {
362
- async upload(file: Buffer, filename: string, options?) {
363
- // Upload to Cloudflare R2
364
- // ...
365
- return { filename, url, size, contentType }
433
+ constructor(private config: CloudflareR2Config) {}
434
+
435
+ async upload(
436
+ file: Buffer | Uint8Array,
437
+ filename: string,
438
+ options?: UploadOptions,
439
+ ): Promise<UploadResult> {
440
+ // Upload to Cloudflare R2 ...
441
+ return {
442
+ filename,
443
+ url: this.getUrl(filename),
444
+ size: file.length,
445
+ contentType: options?.contentType ?? 'application/octet-stream',
446
+ }
366
447
  }
367
448
 
368
- async download(filename: string) {
369
- // Download from R2
370
- // ...
449
+ async download(filename: string): Promise<Buffer> {
450
+ // Download from R2 ...
451
+ return Buffer.from('')
371
452
  }
372
453
 
373
- async delete(filename: string) {
374
- // Delete from R2
375
- // ...
454
+ async delete(filename: string): Promise<void> {
455
+ // Delete from R2 ...
376
456
  }
377
457
 
378
- getUrl(filename: string) {
379
- return `https://r2.example.com/${filename}`
458
+ getUrl(filename: string): string {
459
+ return `https://r2.example.com/${this.config.bucket}/${filename}`
380
460
  }
381
461
  }
462
+ ```
382
463
 
383
- // Register in runtime
384
- import { createStorageProvider } from '@opensaas/stack-storage/runtime'
464
+ ```typescript
465
+ // lib/register-storage.ts (server-only)
466
+ import { registerStorageProvider } from '@opensaas/stack-storage/runtime'
467
+ import { CloudflareR2StorageProvider, type CloudflareR2Config } from './cloudflare-r2-storage'
468
+
469
+ registerStorageProvider<CloudflareR2Config>(
470
+ 'cloudflare-r2',
471
+ (config) => new CloudflareR2StorageProvider(config),
472
+ )
473
+ ```
385
474
 
386
- // Extend createStorageProvider to support 'cloudflare-r2' type
475
+ ```typescript
476
+ // opensaas.config.ts
477
+ export default config({
478
+ storage: {
479
+ media: { type: 'cloudflare-r2', bucket: 'media', accountId: process.env.CF_ACCOUNT_ID! },
480
+ },
481
+ // ...
482
+ })
387
483
  ```
388
484
 
485
+ If a field references a provider whose `type` has not been registered,
486
+ `createStorageProvider` throws a clear error
487
+ (`Unknown storage provider type: <type>. Register it with registerStorageProvider(...)`).
488
+ Reads are unaffected — assembling existing asset metadata only stamps the
489
+ provider **name** and never constructs a provider.
490
+
389
491
  ## Type Safety
390
492
 
391
493
  All types are strongly typed:
@@ -2,7 +2,17 @@ import type { OpenSaasConfig } from '@opensaas/stack-core';
2
2
  import type { StorageProvider, FileMetadata, ImageMetadata, ImageTransformationConfig } from '../config/types.js';
3
3
  import { type FileValidationOptions } from '../utils/upload.js';
4
4
  /**
5
- * Creates a storage provider instance from config
5
+ * Creates a storage provider instance from config.
6
+ *
7
+ * The provider `type` is resolved through the provider registry (see
8
+ * {@link registerStorageProvider}) rather than a closed `switch`. `'local'` is
9
+ * registered as a built-in default, so it works with no registration step.
10
+ * Optional providers (`@opensaas/stack-storage-s3`,
11
+ * `@opensaas/stack-storage-vercel`) and custom providers must be registered by
12
+ * the host before they can be constructed.
13
+ *
14
+ * @throws If the named provider is not present in `config.storage`.
15
+ * @throws If no provider factory has been registered for the config's `type`.
6
16
  */
7
17
  export declare function createStorageProvider(config: OpenSaasConfig, providerName: string): StorageProvider;
8
18
  /**
@@ -72,4 +82,5 @@ export declare function deleteFile(config: OpenSaasConfig, storageProviderName:
72
82
  */
73
83
  export declare function deleteImage(config: OpenSaasConfig, metadata: ImageMetadata): Promise<void>;
74
84
  export { parseFileFromFormData } from '../utils/upload.js';
85
+ export { registerStorageProvider, getStorageProviderFactory, hasStorageProvider, resetStorageProviderRegistry, type StorageProviderFactory, } from './registry.js';
75
86
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,KAAK,EACV,eAAe,EAEf,YAAY,EACZ,aAAa,EACb,yBAAyB,EAC1B,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAA6B,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAG1F;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,MAAM,GACnB,eAAe,CAajB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,yBAAyB;IACzB,UAAU,CAAC,EAAE,qBAAqB,CAAA;IAClC,sBAAsB;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,qCAAqC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAA;CAC5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,mBAAmB,EAAE,MAAM,EAC3B,IAAI,EAAE;IACJ,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf,EACD,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA0CvB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,cAAc,EACtB,mBAAmB,EAAE,MAAM,EAC3B,IAAI,EAAE;IACJ,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf,EACD,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,aAAa,CAAC,CA8DxB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhG;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,aAAa,EACb,yBAAyB,EAC1B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAA6B,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAI1F;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,MAAM,GACnB,eAAe,CAiBjB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,yBAAyB;IACzB,UAAU,CAAC,EAAE,qBAAqB,CAAA;IAClC,sBAAsB;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,qCAAqC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAA;CAC5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,mBAAmB,EAAE,MAAM,EAC3B,IAAI,EAAE;IACJ,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf,EACD,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA0CvB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,cAAc,EACtB,mBAAmB,EAAE,MAAM,EAC3B,IAAI,EAAE;IACJ,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf,EACD,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,aAAa,CAAC,CA8DxB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhG;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAI1D,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,kBAAkB,EAClB,4BAA4B,EAC5B,KAAK,sBAAsB,GAC5B,MAAM,eAAe,CAAA"}
@@ -1,20 +1,31 @@
1
- import { LocalStorageProvider } from '../providers/local.js';
2
1
  import { validateFile, getMimeType } from '../utils/upload.js';
3
2
  import { getImageDimensions, processImageTransformations } from '../utils/image.js';
3
+ import { getStorageProviderFactory } from './registry.js';
4
4
  /**
5
- * Creates a storage provider instance from config
5
+ * Creates a storage provider instance from config.
6
+ *
7
+ * The provider `type` is resolved through the provider registry (see
8
+ * {@link registerStorageProvider}) rather than a closed `switch`. `'local'` is
9
+ * registered as a built-in default, so it works with no registration step.
10
+ * Optional providers (`@opensaas/stack-storage-s3`,
11
+ * `@opensaas/stack-storage-vercel`) and custom providers must be registered by
12
+ * the host before they can be constructed.
13
+ *
14
+ * @throws If the named provider is not present in `config.storage`.
15
+ * @throws If no provider factory has been registered for the config's `type`.
6
16
  */
7
17
  export function createStorageProvider(config, providerName) {
8
18
  if (!config.storage || !config.storage[providerName]) {
9
19
  throw new Error(`Storage provider '${providerName}' not found in config`);
10
20
  }
11
21
  const providerConfig = config.storage[providerName];
12
- switch (providerConfig.type) {
13
- case 'local':
14
- return new LocalStorageProvider(providerConfig);
15
- default:
16
- throw new Error(`Unknown storage provider type: ${providerConfig.type}`);
22
+ const factory = getStorageProviderFactory(providerConfig.type);
23
+ if (!factory) {
24
+ throw new Error(`Unknown storage provider type: ${providerConfig.type}. ` +
25
+ `Register it with registerStorageProvider('${providerConfig.type}', ...) from ` +
26
+ `'@opensaas/stack-storage/runtime' before use.`);
17
27
  }
28
+ return factory(providerConfig);
18
29
  }
19
30
  /**
20
31
  * Uploads a file to the specified storage provider
@@ -154,4 +165,7 @@ export async function deleteImage(config, metadata) {
154
165
  }
155
166
  }
156
167
  export { parseFileFromFormData } from '../utils/upload.js';
168
+ // Provider registration API: hosts register optional/custom providers so
169
+ // createStorageProvider can construct them (see registry.ts).
170
+ export { registerStorageProvider, getStorageProviderFactory, hasStorageProvider, resetStorageProviderRegistry, } from './registry.js';
157
171
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import type { StorageProvider, BaseStorageConfig } from '../config/types.js';
2
+ /**
3
+ * A factory that constructs a {@link StorageProvider} from its provider config.
4
+ *
5
+ * The factory is generic over the concrete config shape so a provider package
6
+ * can register with full type safety (e.g. `S3StorageConfig`), while the
7
+ * registry stores factories against the {@link BaseStorageConfig} contract that
8
+ * every provider config satisfies.
9
+ *
10
+ * @typeParam TConfig - The provider-specific config type (must extend
11
+ * {@link BaseStorageConfig}).
12
+ */
13
+ export type StorageProviderFactory<TConfig extends BaseStorageConfig = BaseStorageConfig> = (config: TConfig) => StorageProvider;
14
+ /**
15
+ * Registers a storage provider factory for a given provider `type`.
16
+ *
17
+ * The host application calls this to opt in to a provider it uses — either one
18
+ * of the optional provider packages (`@opensaas/stack-storage-s3`,
19
+ * `@opensaas/stack-storage-vercel`) or a custom provider. Once registered,
20
+ * {@link createStorageProvider} can construct that provider when a field is
21
+ * bound to it.
22
+ *
23
+ * Registering an already-registered `type` overwrites the previous factory,
24
+ * allowing a host to replace a built-in (e.g. swap the default `'local'`
25
+ * provider) if needed.
26
+ *
27
+ * @typeParam TConfig - The provider-specific config type.
28
+ * @param type - The provider `type` discriminator used in storage config
29
+ * (e.g. `'s3'`, `'vercel-blob'`, `'cloudflare-r2'`).
30
+ * @param factory - A function that builds the provider from its config.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { registerStorageProvider } from '@opensaas/stack-storage/runtime'
35
+ * import { S3StorageProvider, type S3StorageConfig } from '@opensaas/stack-storage-s3'
36
+ *
37
+ * registerStorageProvider<S3StorageConfig>('s3', (config) => new S3StorageProvider(config))
38
+ * ```
39
+ */
40
+ export declare function registerStorageProvider<TConfig extends BaseStorageConfig>(type: string, factory: StorageProviderFactory<TConfig>): void;
41
+ /**
42
+ * Returns the factory registered for a provider `type`, or `undefined` when no
43
+ * provider has been registered under that `type`.
44
+ */
45
+ export declare function getStorageProviderFactory(type: string): StorageProviderFactory | undefined;
46
+ /**
47
+ * Returns `true` when a provider factory is registered for the given `type`.
48
+ */
49
+ export declare function hasStorageProvider(type: string): boolean;
50
+ /**
51
+ * Resets the registry to only the built-in defaults.
52
+ *
53
+ * Primarily useful in tests to guarantee isolation between cases that register
54
+ * custom providers.
55
+ */
56
+ export declare function resetStorageProviderRegistry(): void;
57
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/runtime/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAsB,MAAM,oBAAoB,CAAA;AAGhG;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,CAAC,OAAO,SAAS,iBAAiB,GAAG,iBAAiB,IAAI,CAC1F,MAAM,EAAE,OAAO,KACZ,eAAe,CAAA;AAYpB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,iBAAiB,EACvE,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,sBAAsB,CAAC,OAAO,CAAC,GACvC,IAAI,CAON;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS,CAE1F;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD"}
@@ -0,0 +1,81 @@
1
+ import { LocalStorageProvider } from '../providers/local.js';
2
+ /**
3
+ * Internal registry mapping a provider `type` to its factory.
4
+ *
5
+ * Factories are stored against {@link BaseStorageConfig} because every provider
6
+ * config extends it. `registerStorageProvider` narrows the public input to the
7
+ * concrete config type, then widens it for storage — this conversion happens
8
+ * once, internally, so the public API stays free of `any`/casts.
9
+ */
10
+ const registry = new Map();
11
+ /**
12
+ * Registers a storage provider factory for a given provider `type`.
13
+ *
14
+ * The host application calls this to opt in to a provider it uses — either one
15
+ * of the optional provider packages (`@opensaas/stack-storage-s3`,
16
+ * `@opensaas/stack-storage-vercel`) or a custom provider. Once registered,
17
+ * {@link createStorageProvider} can construct that provider when a field is
18
+ * bound to it.
19
+ *
20
+ * Registering an already-registered `type` overwrites the previous factory,
21
+ * allowing a host to replace a built-in (e.g. swap the default `'local'`
22
+ * provider) if needed.
23
+ *
24
+ * @typeParam TConfig - The provider-specific config type.
25
+ * @param type - The provider `type` discriminator used in storage config
26
+ * (e.g. `'s3'`, `'vercel-blob'`, `'cloudflare-r2'`).
27
+ * @param factory - A function that builds the provider from its config.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { registerStorageProvider } from '@opensaas/stack-storage/runtime'
32
+ * import { S3StorageProvider, type S3StorageConfig } from '@opensaas/stack-storage-s3'
33
+ *
34
+ * registerStorageProvider<S3StorageConfig>('s3', (config) => new S3StorageProvider(config))
35
+ * ```
36
+ */
37
+ export function registerStorageProvider(type, factory) {
38
+ // `factory` accepts a narrower `TConfig`; the registry stores it against the
39
+ // wider `BaseStorageConfig`. This widening wrapper is safe because
40
+ // `createStorageProvider` only ever invokes a factory with the config whose
41
+ // `type` selected it, so the runtime config always matches `TConfig`.
42
+ const widened = (config) => factory(config);
43
+ registry.set(type, widened);
44
+ }
45
+ /**
46
+ * Returns the factory registered for a provider `type`, or `undefined` when no
47
+ * provider has been registered under that `type`.
48
+ */
49
+ export function getStorageProviderFactory(type) {
50
+ return registry.get(type);
51
+ }
52
+ /**
53
+ * Returns `true` when a provider factory is registered for the given `type`.
54
+ */
55
+ export function hasStorageProvider(type) {
56
+ return registry.has(type);
57
+ }
58
+ /**
59
+ * Resets the registry to only the built-in defaults.
60
+ *
61
+ * Primarily useful in tests to guarantee isolation between cases that register
62
+ * custom providers.
63
+ */
64
+ export function resetStorageProviderRegistry() {
65
+ registry.clear();
66
+ registerBuiltinProviders();
67
+ }
68
+ /**
69
+ * Registers the providers that ship with `@opensaas/stack-storage` itself.
70
+ *
71
+ * Currently only `'local'`, which keeps existing behaviour unchanged: hosts get
72
+ * local filesystem storage with no registration step. Optional providers
73
+ * (`s3`, `vercel-blob`) are NOT registered here — the host opts into those to
74
+ * avoid forcing the AWS/Vercel SDKs onto every storage user.
75
+ */
76
+ function registerBuiltinProviders() {
77
+ registerStorageProvider('local', (config) => new LocalStorageProvider(config));
78
+ }
79
+ // Register built-in providers on module load so `'local'` works by default.
80
+ registerBuiltinProviders();
81
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/runtime/registry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAiB5D;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAY,EACZ,OAAwC;IAExC,6EAA6E;IAC7E,mEAAmE;IACnE,4EAA4E;IAC5E,sEAAsE;IACtE,MAAM,OAAO,GAA2B,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,MAAiB,CAAC,CAAA;IAC9E,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAC3B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B;IAC1C,QAAQ,CAAC,KAAK,EAAE,CAAA;IAChB,wBAAwB,EAAE,CAAA;AAC5B,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,wBAAwB;IAC/B,uBAAuB,CAAqB,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAA;AACpG,CAAC;AAED,4EAA4E;AAC5E,wBAAwB,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensaas/stack-storage",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "File and image upload field types with pluggable storage providers for OpenSaas Stack",
5
5
  "type": "module",
6
6
  "exports": {
@@ -35,7 +35,7 @@
35
35
  "@vitest/coverage-v8": "^4.0.18",
36
36
  "typescript": "^5.9.3",
37
37
  "vitest": "^4.1.0",
38
- "@opensaas/stack-core": "0.23.0"
38
+ "@opensaas/stack-core": "0.25.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@opensaas/stack-core": "^0"
@@ -1,17 +1,26 @@
1
1
  import type { OpenSaasConfig } from '@opensaas/stack-core'
2
2
  import type {
3
3
  StorageProvider,
4
- LocalStorageConfig,
5
4
  FileMetadata,
6
5
  ImageMetadata,
7
6
  ImageTransformationConfig,
8
7
  } from '../config/types.js'
9
- import { LocalStorageProvider } from '../providers/local.js'
10
8
  import { validateFile, getMimeType, type FileValidationOptions } from '../utils/upload.js'
11
9
  import { getImageDimensions, processImageTransformations } from '../utils/image.js'
10
+ import { getStorageProviderFactory } from './registry.js'
12
11
 
13
12
  /**
14
- * Creates a storage provider instance from config
13
+ * Creates a storage provider instance from config.
14
+ *
15
+ * The provider `type` is resolved through the provider registry (see
16
+ * {@link registerStorageProvider}) rather than a closed `switch`. `'local'` is
17
+ * registered as a built-in default, so it works with no registration step.
18
+ * Optional providers (`@opensaas/stack-storage-s3`,
19
+ * `@opensaas/stack-storage-vercel`) and custom providers must be registered by
20
+ * the host before they can be constructed.
21
+ *
22
+ * @throws If the named provider is not present in `config.storage`.
23
+ * @throws If no provider factory has been registered for the config's `type`.
15
24
  */
16
25
  export function createStorageProvider(
17
26
  config: OpenSaasConfig,
@@ -23,12 +32,16 @@ export function createStorageProvider(
23
32
 
24
33
  const providerConfig = config.storage[providerName]
25
34
 
26
- switch (providerConfig.type) {
27
- case 'local':
28
- return new LocalStorageProvider(providerConfig as unknown as LocalStorageConfig)
29
- default:
30
- throw new Error(`Unknown storage provider type: ${providerConfig.type}`)
35
+ const factory = getStorageProviderFactory(providerConfig.type)
36
+ if (!factory) {
37
+ throw new Error(
38
+ `Unknown storage provider type: ${providerConfig.type}. ` +
39
+ `Register it with registerStorageProvider('${providerConfig.type}', ...) from ` +
40
+ `'@opensaas/stack-storage/runtime' before use.`,
41
+ )
31
42
  }
43
+
44
+ return factory(providerConfig)
32
45
  }
33
46
 
34
47
  /**
@@ -241,3 +254,13 @@ export async function deleteImage(config: OpenSaasConfig, metadata: ImageMetadat
241
254
  }
242
255
 
243
256
  export { parseFileFromFormData } from '../utils/upload.js'
257
+
258
+ // Provider registration API: hosts register optional/custom providers so
259
+ // createStorageProvider can construct them (see registry.ts).
260
+ export {
261
+ registerStorageProvider,
262
+ getStorageProviderFactory,
263
+ hasStorageProvider,
264
+ resetStorageProviderRegistry,
265
+ type StorageProviderFactory,
266
+ } from './registry.js'
@@ -0,0 +1,106 @@
1
+ import type { StorageProvider, BaseStorageConfig, LocalStorageConfig } from '../config/types.js'
2
+ import { LocalStorageProvider } from '../providers/local.js'
3
+
4
+ /**
5
+ * A factory that constructs a {@link StorageProvider} from its provider config.
6
+ *
7
+ * The factory is generic over the concrete config shape so a provider package
8
+ * can register with full type safety (e.g. `S3StorageConfig`), while the
9
+ * registry stores factories against the {@link BaseStorageConfig} contract that
10
+ * every provider config satisfies.
11
+ *
12
+ * @typeParam TConfig - The provider-specific config type (must extend
13
+ * {@link BaseStorageConfig}).
14
+ */
15
+ export type StorageProviderFactory<TConfig extends BaseStorageConfig = BaseStorageConfig> = (
16
+ config: TConfig,
17
+ ) => StorageProvider
18
+
19
+ /**
20
+ * Internal registry mapping a provider `type` to its factory.
21
+ *
22
+ * Factories are stored against {@link BaseStorageConfig} because every provider
23
+ * config extends it. `registerStorageProvider` narrows the public input to the
24
+ * concrete config type, then widens it for storage — this conversion happens
25
+ * once, internally, so the public API stays free of `any`/casts.
26
+ */
27
+ const registry = new Map<string, StorageProviderFactory>()
28
+
29
+ /**
30
+ * Registers a storage provider factory for a given provider `type`.
31
+ *
32
+ * The host application calls this to opt in to a provider it uses — either one
33
+ * of the optional provider packages (`@opensaas/stack-storage-s3`,
34
+ * `@opensaas/stack-storage-vercel`) or a custom provider. Once registered,
35
+ * {@link createStorageProvider} can construct that provider when a field is
36
+ * bound to it.
37
+ *
38
+ * Registering an already-registered `type` overwrites the previous factory,
39
+ * allowing a host to replace a built-in (e.g. swap the default `'local'`
40
+ * provider) if needed.
41
+ *
42
+ * @typeParam TConfig - The provider-specific config type.
43
+ * @param type - The provider `type` discriminator used in storage config
44
+ * (e.g. `'s3'`, `'vercel-blob'`, `'cloudflare-r2'`).
45
+ * @param factory - A function that builds the provider from its config.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * import { registerStorageProvider } from '@opensaas/stack-storage/runtime'
50
+ * import { S3StorageProvider, type S3StorageConfig } from '@opensaas/stack-storage-s3'
51
+ *
52
+ * registerStorageProvider<S3StorageConfig>('s3', (config) => new S3StorageProvider(config))
53
+ * ```
54
+ */
55
+ export function registerStorageProvider<TConfig extends BaseStorageConfig>(
56
+ type: string,
57
+ factory: StorageProviderFactory<TConfig>,
58
+ ): void {
59
+ // `factory` accepts a narrower `TConfig`; the registry stores it against the
60
+ // wider `BaseStorageConfig`. This widening wrapper is safe because
61
+ // `createStorageProvider` only ever invokes a factory with the config whose
62
+ // `type` selected it, so the runtime config always matches `TConfig`.
63
+ const widened: StorageProviderFactory = (config) => factory(config as TConfig)
64
+ registry.set(type, widened)
65
+ }
66
+
67
+ /**
68
+ * Returns the factory registered for a provider `type`, or `undefined` when no
69
+ * provider has been registered under that `type`.
70
+ */
71
+ export function getStorageProviderFactory(type: string): StorageProviderFactory | undefined {
72
+ return registry.get(type)
73
+ }
74
+
75
+ /**
76
+ * Returns `true` when a provider factory is registered for the given `type`.
77
+ */
78
+ export function hasStorageProvider(type: string): boolean {
79
+ return registry.has(type)
80
+ }
81
+
82
+ /**
83
+ * Resets the registry to only the built-in defaults.
84
+ *
85
+ * Primarily useful in tests to guarantee isolation between cases that register
86
+ * custom providers.
87
+ */
88
+ export function resetStorageProviderRegistry(): void {
89
+ registry.clear()
90
+ registerBuiltinProviders()
91
+ }
92
+
93
+ /**
94
+ * Registers the providers that ship with `@opensaas/stack-storage` itself.
95
+ *
96
+ * Currently only `'local'`, which keeps existing behaviour unchanged: hosts get
97
+ * local filesystem storage with no registration step. Optional providers
98
+ * (`s3`, `vercel-blob`) are NOT registered here — the host opts into those to
99
+ * avoid forcing the AWS/Vercel SDKs onto every storage user.
100
+ */
101
+ function registerBuiltinProviders(): void {
102
+ registerStorageProvider<LocalStorageConfig>('local', (config) => new LocalStorageProvider(config))
103
+ }
104
+
105
+ // Register built-in providers on module load so `'local'` works by default.
106
+ registerBuiltinProviders()
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import type { OpenSaasConfig } from '@opensaas/stack-core'
3
+ import {
4
+ createStorageProvider,
5
+ registerStorageProvider,
6
+ getStorageProviderFactory,
7
+ hasStorageProvider,
8
+ resetStorageProviderRegistry,
9
+ } from '../src/runtime/index.js'
10
+ import { LocalStorageProvider } from '../src/providers/local.js'
11
+ import { assembleImageMetadata, keystoneImageColumnMap } from '../src/utils/multi-column.js'
12
+ import type {
13
+ StorageProvider,
14
+ UploadOptions,
15
+ UploadResult,
16
+ BaseStorageConfig,
17
+ } from '../src/config/types.js'
18
+
19
+ /**
20
+ * A minimal custom provider config + implementation used to prove that a
21
+ * non-`local`, host-registered provider can be constructed via the registry —
22
+ * without `@opensaas/stack-storage` depending on `-s3`/`-vercel`.
23
+ */
24
+ interface FakeStorageConfig extends BaseStorageConfig {
25
+ type: 'fake'
26
+ baseUrl: string
27
+ }
28
+
29
+ class FakeStorageProvider implements StorageProvider {
30
+ constructor(public readonly config: FakeStorageConfig) {}
31
+
32
+ async upload(
33
+ _file: Buffer | Uint8Array,
34
+ filename: string,
35
+ options?: UploadOptions,
36
+ ): Promise<UploadResult> {
37
+ return {
38
+ filename,
39
+ url: `${this.config.baseUrl}/${filename}`,
40
+ size: 0,
41
+ contentType: options?.contentType ?? 'application/octet-stream',
42
+ }
43
+ }
44
+
45
+ async download(): Promise<Buffer> {
46
+ return Buffer.from('')
47
+ }
48
+
49
+ async delete(): Promise<void> {}
50
+
51
+ getUrl(filename: string): string {
52
+ return `${this.config.baseUrl}/${filename}`
53
+ }
54
+ }
55
+
56
+ /** Builds a minimal, strongly-typed config carrying a single storage provider. */
57
+ function configWith(name: string, providerConfig: BaseStorageConfig): OpenSaasConfig {
58
+ return {
59
+ db: { provider: 'sqlite' },
60
+ lists: {},
61
+ storage: { [name]: providerConfig },
62
+ }
63
+ }
64
+
65
+ describe('storage provider registry', () => {
66
+ beforeEach(() => {
67
+ // Restore registry to built-in defaults so registrations don't leak.
68
+ resetStorageProviderRegistry()
69
+ })
70
+
71
+ describe('built-in defaults', () => {
72
+ it("registers 'local' by default so createStorageProvider builds it", () => {
73
+ const config = configWith('files', {
74
+ type: 'local',
75
+ uploadDir: './uploads',
76
+ serveUrl: '/uploads',
77
+ })
78
+
79
+ const provider = createStorageProvider(config, 'files')
80
+
81
+ expect(provider).toBeInstanceOf(LocalStorageProvider)
82
+ })
83
+
84
+ it("reports 'local' as registered", () => {
85
+ expect(hasStorageProvider('local')).toBe(true)
86
+ expect(getStorageProviderFactory('local')).toBeTypeOf('function')
87
+ })
88
+ })
89
+
90
+ describe('registering a custom provider', () => {
91
+ it('lets createStorageProvider construct a registered non-local provider', () => {
92
+ registerStorageProvider<FakeStorageConfig>(
93
+ 'fake',
94
+ (providerConfig) => new FakeStorageProvider(providerConfig),
95
+ )
96
+
97
+ const config = configWith('cdn', {
98
+ type: 'fake',
99
+ baseUrl: 'https://cdn.example.com',
100
+ })
101
+
102
+ const provider = createStorageProvider(config, 'cdn')
103
+
104
+ expect(provider).toBeInstanceOf(FakeStorageProvider)
105
+ expect(provider.getUrl('photo.jpg')).toBe('https://cdn.example.com/photo.jpg')
106
+ })
107
+
108
+ it('passes the provider config through to the factory', () => {
109
+ let received: FakeStorageConfig | undefined
110
+ registerStorageProvider<FakeStorageConfig>('fake', (providerConfig) => {
111
+ received = providerConfig
112
+ return new FakeStorageProvider(providerConfig)
113
+ })
114
+
115
+ const providerConfig: FakeStorageConfig = {
116
+ type: 'fake',
117
+ baseUrl: 'https://assets.example.com',
118
+ }
119
+
120
+ createStorageProvider(configWith('assets', providerConfig), 'assets')
121
+
122
+ expect(received).toEqual(providerConfig)
123
+ })
124
+
125
+ it('reflects registration via hasStorageProvider/getStorageProviderFactory', () => {
126
+ expect(hasStorageProvider('fake')).toBe(false)
127
+
128
+ registerStorageProvider<FakeStorageConfig>(
129
+ 'fake',
130
+ (providerConfig) => new FakeStorageProvider(providerConfig),
131
+ )
132
+
133
+ expect(hasStorageProvider('fake')).toBe(true)
134
+ expect(getStorageProviderFactory('fake')).toBeTypeOf('function')
135
+ })
136
+
137
+ it('overwrites a previous registration for the same type', () => {
138
+ registerStorageProvider<FakeStorageConfig>(
139
+ 'fake',
140
+ (providerConfig) => new FakeStorageProvider({ ...providerConfig, baseUrl: 'first' }),
141
+ )
142
+ registerStorageProvider<FakeStorageConfig>(
143
+ 'fake',
144
+ (providerConfig) => new FakeStorageProvider({ ...providerConfig, baseUrl: 'second' }),
145
+ )
146
+
147
+ const provider = createStorageProvider(
148
+ configWith('cdn', { type: 'fake', baseUrl: 'ignored' }),
149
+ 'cdn',
150
+ )
151
+
152
+ expect(provider.getUrl('x')).toBe('second/x')
153
+ })
154
+ })
155
+
156
+ describe('unregistered providers', () => {
157
+ it('throws a clear error for an unregistered type', () => {
158
+ const config = configWith('cdn', { type: 's3', bucket: 'my-bucket' })
159
+
160
+ expect(() => createStorageProvider(config, 'cdn')).toThrow(
161
+ /Unknown storage provider type: s3/,
162
+ )
163
+ expect(() => createStorageProvider(config, 'cdn')).toThrow(/registerStorageProvider/)
164
+ })
165
+
166
+ it('still throws when the named provider is absent from config', () => {
167
+ const config = configWith('files', {
168
+ type: 'local',
169
+ uploadDir: './uploads',
170
+ serveUrl: '/uploads',
171
+ })
172
+
173
+ expect(() => createStorageProvider(config, 'missing')).toThrow(
174
+ /Storage provider 'missing' not found in config/,
175
+ )
176
+ })
177
+ })
178
+
179
+ describe('read path is unaffected by the registry', () => {
180
+ it('assembleImageMetadata stamps the provider name without constructing a provider', () => {
181
+ // No provider registered for 'remote'; the read path must not care.
182
+ expect(hasStorageProvider('remote')).toBe(false)
183
+
184
+ const map = keystoneImageColumnMap('image')
185
+ const row = {
186
+ image_url: 'https://cdn.example.com/pic.jpg',
187
+ image_width: 800,
188
+ image_height: 600,
189
+ image_filesize: 1234,
190
+ image_contentType: 'image/jpeg',
191
+ image_pathname: 'pic.jpg',
192
+ }
193
+
194
+ const meta = assembleImageMetadata(row, map, 'remote')
195
+
196
+ expect(meta).not.toBeNull()
197
+ expect(meta?.storageProvider).toBe('remote')
198
+ expect(meta?.url).toBe('https://cdn.example.com/pic.jpg')
199
+ })
200
+ })
201
+ })