@_linked/core 2.19.0 → 2.20.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.20.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#235](https://github.com/linked-fw/core/pull/235) [`19624f7`](https://github.com/linked-fw/core/commit/19624f7bb7b1d83bd99d9797a7da41fe7e09432b) Thanks [@flyon](https://github.com/flyon)! - Point `repository.url` at the linked-fw organisation, so npm provenance verification matches the repository that builds the package.
8
+
9
+ ## 2.20.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#230](https://github.com/linked-fw/core/pull/230) [`bf8df0c`](https://github.com/linked-fw/core/commit/bf8df0c043cdb588e4b9f8baf97f87b340f0078b) Thanks [@flyon](https://github.com/flyon)! - Purpose-named file stores, save options and `statFile`.
14
+
15
+ - `LinkedFileStorage` gains a purpose registry: `registerPurpose`, `setStore`,
16
+ `getStore`, `hasStore`, `listPurposes` and `accessURLFor`. `getStore(purpose)`
17
+ returns the store configured for that purpose; a purpose that is registered
18
+ but not configured falls back to the default store (noted once with
19
+ `console.debug`); an unregistered
20
+ purpose throws, listing the known purposes, so a typo cannot silently write to
21
+ the wrong store. The well-known purposes `uploads` and `appAssets` are exported
22
+ as `FileStorePurposes` and registered at module load. `uploads` is an ordinary
23
+ purpose: `setDefaultStore` configures no purpose of its own, so `uploads`
24
+ resolves to the default store as a fallback, `hasStore('uploads')` is true only
25
+ after an explicit `setStore('uploads', store)`, and that call has the same
26
+ effect whether it runs before or after `setDefaultStore`.
27
+ - `getDefaultDataset`/`setDefaultDataset` are renamed to
28
+ `getDefaultStore`/`setDefaultStore`. The old names remain as `@deprecated`
29
+ aliases that forward, so no caller has to change; they will be dropped in the
30
+ next major.
31
+ - `IFileStore.saveFile` now takes `SaveFileOptions` (`mimeType`, `cacheControl`,
32
+ `metadata`, `preventDuplicates`) as its third argument, still accepting a
33
+ `string` there as the old positional `mimeType`. `normalizeSaveFileOptions` is
34
+ exported for implementations; it reports `preventDuplicates` as `undefined`
35
+ when the caller did not specify one — there is no core-wide default, so each
36
+ store keeps applying its own (`S3FileStore` overwrites, `LocalFileStore` adds a
37
+ random suffix). `options.preventDuplicates` wins over the positional argument.
38
+ `LinkedFileStorage.saveFile` likewise forwards an unspecified
39
+ `preventDuplicates` as `undefined` rather than `false`, so a
40
+ `saveFile(path, bytes)` call keeps behaving exactly as before.
41
+ - `IFileStore` gains an optional `statFile(filePath): Promise<FileStat | null>`
42
+ for verify-after-upload. Existing implementations stay valid.
43
+
44
+ ## 2.19.1
45
+
46
+ ### Patch Changes
47
+
48
+ - [#231](https://github.com/linked-fw/core/pull/231) [`58a5b7a`](https://github.com/linked-fw/core/commit/58a5b7af7332584e9f40c16d17e303f18d86962a) Thanks [@flyon](https://github.com/flyon)! - Declare npm as the package manager for this repo and mark `package-lock.json` as a generated file.
49
+
3
50
  ## 2.19.0
4
51
 
5
52
  ### Minor Changes
package/README.md CHANGED
@@ -362,6 +362,86 @@ LinkedStorage.setDefaultDataset(stores.appData);
362
362
 
363
363
  Each store class must accept a single config-object argument (`new StoreClass(config)`). `@_linked/fuseki`'s `FusekiStore` and `@_linked/server`'s `BackendAPIStore` both follow this contract.
364
364
 
365
+ ## File storage
366
+
367
+ `LinkedStorage` routes *quads*; `LinkedFileStorage` routes *files*. Files are addressed by **purpose** — a short string naming what the files are for (`uploads`, `appAssets`, …) — so an app can send release bundles to a CDN bucket and user uploads to another store without any caller knowing which.
368
+
369
+ ```ts
370
+ import {LinkedFileStorage, FileStorePurposes} from '@_linked/core';
371
+ import {S3FileStore} from '@_linked/s3';
372
+
373
+ LinkedFileStorage.setDefaultStore(new S3FileStore({/* … */})); // fallback for every purpose
374
+ LinkedFileStorage.setStore(FileStorePurposes.appAssets, new S3FileStore({/* cdn */}));
375
+
376
+ await LinkedFileStorage.getStore(FileStorePurposes.appAssets).saveFile('/app.js', bytes);
377
+ LinkedFileStorage.accessURLFor(FileStorePurposes.appAssets); // → https://cdn.example.com
378
+ ```
379
+
380
+ ### Resolution rule
381
+
382
+ `getStore(purpose)` resolves in one order, and there is no fourth case:
383
+
384
+ 1. **configured** — a store was passed to `setStore(purpose, store)` → that store;
385
+ 2. **registered but not configured** → the default store, with a one-time `console.debug` line saying the purpose has no dedicated store (the normal case for a single-store app; it throws instead if no default store is set);
386
+ 3. **not registered** → throws, listing the known purposes. A typo like `'appAsset'` must not silently write release bundles into the uploads store.
387
+
388
+ `uploads` is an ordinary purpose with no special case: `setDefaultStore` configures *no* purpose, so `uploads` reaches the default store through the fallback in step 2, and `setStore('uploads', other)` wins whether it is called before or after `setDefaultStore`.
389
+
390
+ | Method | Meaning |
391
+ | --- | --- |
392
+ | `registerPurpose(purpose, {description?})` | Declare a purpose without configuring a store. |
393
+ | `setStore(purpose, store)` | Configure (and register) the store; calls `store.init()`. |
394
+ | `getStore(purpose)` | Resolve per the rule above. A **live reference** — call it per operation if config can change. |
395
+ | `hasStore(purpose)` | Whether a store was *explicitly configured*. Ignores the fallback, so it is `false` while `getStore` still works. Never use it as a guard before `getStore`. |
396
+ | `listPurposes()` | `{purpose, description?, configured}` for every declared purpose. |
397
+ | `accessURLFor(purpose)` | The `accessURL` of the resolved store. |
398
+ | `getDefaultStore()` / `setDefaultStore(store)` | The fallback store. `getDefaultDataset`/`setDefaultDataset` are `@deprecated` aliases that forward, kept until the next major. |
399
+
400
+ ### Introducing a purpose from a package
401
+
402
+ Export the constant and register it **in the same module**, so importing the constant *is* the registration and load order resolves itself:
403
+
404
+ ```ts
405
+ // @_linked/captures/purposes.ts
406
+ import {LinkedFileStorage} from '@_linked/core';
407
+
408
+ export const CapturesPurpose = 'captures';
409
+ LinkedFileStorage.registerPurpose(CapturesPurpose, {description: '3D capture artifacts.'});
410
+ ```
411
+
412
+ A consumer that imports `CapturesPurpose` can call `getStore(CapturesPurpose)` immediately; an app that never splits it out simply gets the default store. Core ships only `FileStorePurposes.uploads` and `FileStorePurposes.appAssets` — a new purpose belongs in the package that owns the concept.
413
+
414
+ ### Saving files: `SaveFileOptions`
415
+
416
+ ```ts
417
+ await store.saveFile('/app.js', bytes, {
418
+ mimeType: 'application/javascript',
419
+ cacheControl: 'public, max-age=31536000, immutable',
420
+ metadata: {release: '1.2.3'},
421
+ preventDuplicates: true, // rename instead of overwriting when the path is taken
422
+ });
423
+
424
+ // Backwards compatible: a string third argument is read as the positional mimeType.
425
+ await store.saveFile('/photo.webp', bytes, 'image/webp', true);
426
+ ```
427
+
428
+ `options.preventDuplicates` wins over the positional fourth argument. Implementations should start with `normalizeSaveFileOptions(options, preventDuplicates)`, which collapses both forms into one object.
429
+
430
+ > **There is no core-wide `preventDuplicates` default.** When a caller does not specify one it stays `undefined` all the way to the store, and **each store applies its own**: `LocalFileStore` adds a random suffix, `S3FileStore` overwrites. Pass it explicitly whenever the behaviour matters — never assume a default.
431
+
432
+ ### Optional `statFile`
433
+
434
+ ```ts
435
+ const stat = await store.statFile?.('/app.js'); // FileStat | null | undefined
436
+ if (!stat) {
437
+ // Store cannot report metadata, or the file is absent — skip verification.
438
+ } else if (stat.sha256) {
439
+ // Only a real content hash verifies. `etag` is not one on S3.
440
+ }
441
+ ```
442
+
443
+ `statFile` is optional by design: a store without it still publishes. A caller must treat a missing method (`undefined`) or a missing `sha256` as "cannot verify" and continue, rather than failing the upload or falling back to `etag`.
444
+
365
445
  ## Automatic data validation
366
446
 
367
447
  SHACL shapes are ideal for data validation. Linked generates SHACL shapes from your TypeScript Shape classes, which you can sync to your store for schema-level validation. When your store enforces those shapes at runtime, you get both schema validation and runtime enforcement for extra safety.
@@ -1,4 +1,38 @@
1
1
  import type { Readable } from 'stream';
2
+ /**
3
+ * Options for `IFileStore.saveFile`.
4
+ *
5
+ * Passed as the third argument. A plain `string` is still accepted there and is
6
+ * read as the old positional `mimeType`, so existing callers keep working.
7
+ */
8
+ export interface SaveFileOptions {
9
+ /** Content type of the stored bytes, e.g. `image/webp`. */
10
+ mimeType?: string;
11
+ /** Cache header the store should attach, e.g. `public, max-age=31536000, immutable`. */
12
+ cacheControl?: string;
13
+ /** Store-level user metadata, when the backing store supports it. */
14
+ metadata?: Record<string, string>;
15
+ /**
16
+ * Rename instead of overwriting when the path is already taken.
17
+ *
18
+ * Left `undefined` when the caller did not say: each store then applies its
19
+ * own default (`S3FileStore` overwrites, `LocalFileStore` adds a suffix).
20
+ */
21
+ preventDuplicates?: boolean;
22
+ }
23
+ /**
24
+ * Metadata of a stored file, for verify-after-upload.
25
+ *
26
+ * `sha256` is only set when the store can actually report a content hash; a
27
+ * caller that does not get one must treat the file as "cannot verify" rather
28
+ * than falling back to `etag`, which is not a content hash on S3.
29
+ */
30
+ export interface FileStat {
31
+ size: number;
32
+ /** Only when the store can report it. */
33
+ sha256?: string;
34
+ etag?: string;
35
+ }
2
36
  export interface IFileStore {
3
37
  /**
4
38
  * The base URL to access the filestore's files. For example:
@@ -12,5 +46,34 @@ export interface IFileStore {
12
46
  fileExists(filePath: string): Promise<boolean>;
13
47
  getFile(filePath: string): Promise<Buffer | null>;
14
48
  listFiles(prefix?: string): Promise<string[]>;
15
- saveFile(filePath: string, fileContent: string | Uint8Array | Buffer | Readable, mimeType?: string, preventDuplicates?: boolean): Promise<string | null>;
49
+ saveFile(filePath: string, fileContent: string | Uint8Array | Buffer | Readable,
50
+ /** `SaveFileOptions`, or a `string` read as the old positional `mimeType`. */
51
+ options?: SaveFileOptions | string,
52
+ /** Only honoured when `options` is a string; otherwise use `options.preventDuplicates`. */
53
+ preventDuplicates?: boolean): Promise<string | null>;
54
+ /**
55
+ * Optional. Metadata of a stored file, for verify-after-upload.
56
+ * Resolves to `null` when the file does not exist.
57
+ *
58
+ * Optional by design (plan 011 D7): a store without it still publishes, and
59
+ * the caller simply skips verification.
60
+ */
61
+ statFile?(filePath: string): Promise<FileStat | null>;
16
62
  }
63
+ /**
64
+ * Normalise `saveFile`'s widened third argument into a single options object.
65
+ * Every `IFileStore` implementation should call this first, so the positional
66
+ * mime-type form and the options form cannot drift apart.
67
+ *
68
+ * `preventDuplicates` stays `undefined` when neither `options.preventDuplicates`
69
+ * nor the positional argument was given: "unspecified" travels end to end and
70
+ * **each store applies its own default** (`S3FileStore` overwrites,
71
+ * `LocalFileStore` adds a random suffix). This helper must never invent `false`,
72
+ * or a two-argument `saveFile(path, bytes)` would silently start overwriting.
73
+ *
74
+ * `options.preventDuplicates` wins over the positional argument when both are
75
+ * given.
76
+ */
77
+ export declare function normalizeSaveFileOptions(options?: SaveFileOptions | string, preventDuplicates?: boolean): SaveFileOptions & {
78
+ preventDuplicates?: boolean | undefined;
79
+ };
@@ -1,2 +1,22 @@
1
- export {};
1
+ /**
2
+ * Normalise `saveFile`'s widened third argument into a single options object.
3
+ * Every `IFileStore` implementation should call this first, so the positional
4
+ * mime-type form and the options form cannot drift apart.
5
+ *
6
+ * `preventDuplicates` stays `undefined` when neither `options.preventDuplicates`
7
+ * nor the positional argument was given: "unspecified" travels end to end and
8
+ * **each store applies its own default** (`S3FileStore` overwrites,
9
+ * `LocalFileStore` adds a random suffix). This helper must never invent `false`,
10
+ * or a two-argument `saveFile(path, bytes)` would silently start overwriting.
11
+ *
12
+ * `options.preventDuplicates` wins over the positional argument when both are
13
+ * given.
14
+ */
15
+ export function normalizeSaveFileOptions(options, preventDuplicates) {
16
+ var _a;
17
+ if (typeof options === 'string') {
18
+ return { mimeType: options, preventDuplicates };
19
+ }
20
+ return Object.assign(Object.assign({}, (options !== null && options !== void 0 ? options : {})), { preventDuplicates: (_a = options === null || options === void 0 ? void 0 : options.preventDuplicates) !== null && _a !== void 0 ? _a : preventDuplicates });
21
+ }
2
22
  //# sourceMappingURL=IFileStore.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"IFileStore.js","sourceRoot":"","sources":["../../../src/interfaces/IFileStore.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"IFileStore.js","sourceRoot":"","sources":["../../../src/interfaces/IFileStore.ts"],"names":[],"mappings":"AA4EA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAkC,EAClC,iBAA2B;;IAE3B,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,EAAC,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAC,CAAC;IAChD,CAAC;IACD,uCACK,CAAC,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,CAAC,KAClB,iBAAiB,EAAE,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,iBAAiB,mCAAI,iBAAiB,IAClE;AACJ,CAAC"}
@@ -1,17 +1,110 @@
1
- import type { IFileStore } from '../interfaces/IFileStore.js';
1
+ import type { IFileStore, SaveFileOptions } from '../interfaces/IFileStore.js';
2
2
  import type { Readable } from 'stream';
3
+ /**
4
+ * Well-known file store purposes shipped with core.
5
+ *
6
+ * Core itself never writes to `appAssets`; it lives here as the documented
7
+ * convention (plan 011 D4) so the CLI and apps agree on the same key without a
8
+ * shared runtime dependency. Any *new* purpose's constant belongs in the
9
+ * runtime package that owns the concept, not here and not in the CLI.
10
+ */
11
+ export declare const FileStorePurposes: {
12
+ /** User-generated files. Resolves to the default store unless split out. */
13
+ readonly uploads: "uploads";
14
+ /** Built app bundles published by `linked build-app`. */
15
+ readonly appAssets: "appAssets";
16
+ };
17
+ export type FileStorePurpose = (typeof FileStorePurposes)[keyof typeof FileStorePurposes];
18
+ export interface FileStorePurposeInfo {
19
+ description?: string;
20
+ }
21
+ export interface FileStorePurposeListing {
22
+ purpose: string;
23
+ description?: string;
24
+ configured: boolean;
25
+ }
3
26
  export declare abstract class LinkedFileStorage {
4
- private static defaultDataset;
27
+ private static defaultStore;
5
28
  private static url;
29
+ /** Purpose → the store configured for it. */
30
+ private static stores;
31
+ /** Every declared purpose, configured or not. */
32
+ private static purposes;
33
+ /** Purposes already reported as falling back, so the log fires once each. */
34
+ private static loggedFallbacks;
6
35
  static get accessURL(): string;
7
36
  static setDefaultAccessURL(accessURL: string): string;
37
+ static getDefaultStore(): IFileStore;
38
+ /**
39
+ * Configure the default store: the fallback for *every* purpose, `uploads`
40
+ * included.
41
+ *
42
+ * It deliberately writes no entry into the purpose → store map, so `uploads`
43
+ * behaves exactly like `appAssets`: an explicit `setStore('uploads', other)`
44
+ * wins whenever it is called, before or after this, and `hasStore('uploads')`
45
+ * stays false until such a call is made.
46
+ */
47
+ static setDefaultStore(store: IFileStore): void;
48
+ /** @deprecated use {@link LinkedFileStorage.getDefaultStore} */
8
49
  static getDefaultDataset(): IFileStore;
50
+ /** @deprecated use {@link LinkedFileStorage.setDefaultStore} */
9
51
  static setDefaultDataset(dataset: IFileStore): void;
52
+ /**
53
+ * Declare a purpose without configuring a store for it.
54
+ *
55
+ * A package that *reads* a purpose the app may leave unconfigured registers
56
+ * it in the same module that exports its constant, so importing the constant
57
+ * is the registration and ordering resolves itself.
58
+ */
59
+ static registerPurpose(purpose: string, info?: FileStorePurposeInfo): void;
60
+ /** Configure the store for a purpose. Also registers the purpose. */
61
+ static setStore(purpose: string, store: IFileStore): void;
62
+ /**
63
+ * Resolve the store for a purpose (plan 011 D2):
64
+ * - configured → that store;
65
+ * - registered, not configured → the default store, logged once;
66
+ * - not registered → throws.
67
+ *
68
+ * Unregistered throws on purpose: only a registry can tell "the app did not
69
+ * split this purpose out" apart from a typo. Without it, `'appAsset'` would
70
+ * silently write release bundles into the uploads store.
71
+ *
72
+ * The returned store is a **live reference**, resolved once at call time. A
73
+ * caller that holds on to a store returned through the default-store fallback
74
+ * keeps that object even after a later `setStore(purpose, other)`; call
75
+ * `getStore` again per operation if the configuration can change at runtime.
76
+ */
77
+ static getStore(purpose: string): IFileStore;
78
+ /**
79
+ * Whether a store was *explicitly configured* for this purpose.
80
+ *
81
+ * The default-store fallback is deliberately ignored: `hasStore(p)` is false
82
+ * while `getStore(p)` still returns a usable store via the default. Use this
83
+ * to ask "did the app split this purpose out?", never as a guard before
84
+ * `getStore` — that would skip a perfectly working fallback.
85
+ */
86
+ static hasStore(purpose: string): boolean;
87
+ /** Every declared purpose, with whether it has its own store. */
88
+ static listPurposes(): FileStorePurposeListing[];
89
+ /** The access URL of the store resolved for this purpose. */
90
+ static accessURLFor(purpose: string): string;
10
91
  static deleteFile(filePath: string): Promise<void>;
11
92
  static fileExists(filePath: string): Promise<boolean>;
12
93
  static getFile(filePath: string): Promise<Buffer>;
13
94
  static listFiles(prefix?: string): Promise<string[]>;
14
- static saveFile(filePath: string, fileContent: string | Uint8Array | Buffer | Readable, mimeType?: string, preventDuplicates?: boolean): Promise<string>;
95
+ static saveFile(filePath: string, fileContent: string | Uint8Array | Buffer | Readable, options?: SaveFileOptions | string,
96
+ /**
97
+ * Forwarded exactly as given. No default here on purpose: "unspecified"
98
+ * must reach the store so it can apply its own default.
99
+ */
100
+ preventDuplicates?: boolean): Promise<string>;
101
+ /**
102
+ * Clears the module-global registry and default store.
103
+ * For tests only — the registry is process-wide state.
104
+ *
105
+ * @internal
106
+ */
107
+ static resetForTests(): void;
15
108
  }
16
109
  /**
17
110
  * Get the full path of an asset based on the way LinkedFileStorage is configured
@@ -1,39 +1,191 @@
1
+ /**
2
+ * Well-known file store purposes shipped with core.
3
+ *
4
+ * Core itself never writes to `appAssets`; it lives here as the documented
5
+ * convention (plan 011 D4) so the CLI and apps agree on the same key without a
6
+ * shared runtime dependency. Any *new* purpose's constant belongs in the
7
+ * runtime package that owns the concept, not here and not in the CLI.
8
+ */
9
+ export const FileStorePurposes = {
10
+ /** User-generated files. Resolves to the default store unless split out. */
11
+ uploads: 'uploads',
12
+ /** Built app bundles published by `linked build-app`. */
13
+ appAssets: 'appAssets',
14
+ };
1
15
  export class LinkedFileStorage {
2
16
  static get accessURL() {
3
17
  // check if default store is not set, return default accessURL
4
- if (!this.defaultDataset) {
18
+ if (!this.defaultStore) {
5
19
  return this.url;
6
20
  }
7
- return this.defaultDataset.accessURL;
21
+ return this.defaultStore.accessURL;
8
22
  }
9
23
  static setDefaultAccessURL(accessURL) {
10
24
  return (this.url = accessURL);
11
25
  }
26
+ static getDefaultStore() {
27
+ return this.defaultStore;
28
+ }
29
+ /**
30
+ * Configure the default store: the fallback for *every* purpose, `uploads`
31
+ * included.
32
+ *
33
+ * It deliberately writes no entry into the purpose → store map, so `uploads`
34
+ * behaves exactly like `appAssets`: an explicit `setStore('uploads', other)`
35
+ * wins whenever it is called, before or after this, and `hasStore('uploads')`
36
+ * stays false until such a call is made.
37
+ */
38
+ static setDefaultStore(store) {
39
+ this.defaultStore = store;
40
+ if (this.defaultStore.init) {
41
+ this.defaultStore.init();
42
+ }
43
+ }
44
+ /** @deprecated use {@link LinkedFileStorage.getDefaultStore} */
12
45
  static getDefaultDataset() {
13
- return this.defaultDataset;
46
+ return this.getDefaultStore();
14
47
  }
48
+ /** @deprecated use {@link LinkedFileStorage.setDefaultStore} */
15
49
  static setDefaultDataset(dataset) {
16
- this.defaultDataset = dataset;
17
- if (this.defaultDataset.init) {
18
- this.defaultDataset.init();
50
+ this.setDefaultStore(dataset);
51
+ }
52
+ /**
53
+ * Declare a purpose without configuring a store for it.
54
+ *
55
+ * A package that *reads* a purpose the app may leave unconfigured registers
56
+ * it in the same module that exports its constant, so importing the constant
57
+ * is the registration and ordering resolves itself.
58
+ */
59
+ static registerPurpose(purpose, info = {}) {
60
+ var _a;
61
+ const existing = this.purposes.get(purpose);
62
+ // Keep an existing description rather than blanking it on a re-register.
63
+ this.purposes.set(purpose, {
64
+ description: (_a = info.description) !== null && _a !== void 0 ? _a : existing === null || existing === void 0 ? void 0 : existing.description,
65
+ });
66
+ }
67
+ /** Configure the store for a purpose. Also registers the purpose. */
68
+ static setStore(purpose, store) {
69
+ this.registerPurpose(purpose);
70
+ this.stores.set(purpose, store);
71
+ if (store.init) {
72
+ store.init();
19
73
  }
20
74
  }
75
+ /**
76
+ * Resolve the store for a purpose (plan 011 D2):
77
+ * - configured → that store;
78
+ * - registered, not configured → the default store, logged once;
79
+ * - not registered → throws.
80
+ *
81
+ * Unregistered throws on purpose: only a registry can tell "the app did not
82
+ * split this purpose out" apart from a typo. Without it, `'appAsset'` would
83
+ * silently write release bundles into the uploads store.
84
+ *
85
+ * The returned store is a **live reference**, resolved once at call time. A
86
+ * caller that holds on to a store returned through the default-store fallback
87
+ * keeps that object even after a later `setStore(purpose, other)`; call
88
+ * `getStore` again per operation if the configuration can change at runtime.
89
+ */
90
+ static getStore(purpose) {
91
+ const configured = this.stores.get(purpose);
92
+ if (configured) {
93
+ return configured;
94
+ }
95
+ if (!this.purposes.has(purpose)) {
96
+ throw new Error(`Unknown file store purpose '${purpose}'. Register it with LinkedFileStorage.setStore(), ` +
97
+ `or import the package that defines it. Known purposes: ${this.listPurposes()
98
+ .map((entry) => entry.purpose)
99
+ .join(', ') || '(none)'}`);
100
+ }
101
+ if (!this.defaultStore) {
102
+ throw new Error(`No file store is configured for purpose '${purpose}', and no default store is configured. ` +
103
+ `Call LinkedFileStorage.setStore('${purpose}', store) or LinkedFileStorage.setDefaultStore(store).`);
104
+ }
105
+ if (!this.loggedFallbacks.has(purpose)) {
106
+ this.loggedFallbacks.add(purpose);
107
+ // `debug`, not `info`: falling back is the normal configuration for an
108
+ // app that runs one store, so this must not read as a problem or show up
109
+ // in default-level logs. It stays available when diagnosing which store a
110
+ // purpose actually resolved to.
111
+ console.debug(`[LinkedFileStorage] Purpose '${purpose}' has no dedicated store; using the default store.`);
112
+ }
113
+ return this.defaultStore;
114
+ }
115
+ /**
116
+ * Whether a store was *explicitly configured* for this purpose.
117
+ *
118
+ * The default-store fallback is deliberately ignored: `hasStore(p)` is false
119
+ * while `getStore(p)` still returns a usable store via the default. Use this
120
+ * to ask "did the app split this purpose out?", never as a guard before
121
+ * `getStore` — that would skip a perfectly working fallback.
122
+ */
123
+ static hasStore(purpose) {
124
+ return this.stores.has(purpose);
125
+ }
126
+ /** Every declared purpose, with whether it has its own store. */
127
+ static listPurposes() {
128
+ return Array.from(this.purposes.entries()).map(([purpose, info]) => ({
129
+ purpose,
130
+ description: info.description,
131
+ configured: this.stores.has(purpose),
132
+ }));
133
+ }
134
+ /** The access URL of the store resolved for this purpose. */
135
+ static accessURLFor(purpose) {
136
+ return this.getStore(purpose).accessURL;
137
+ }
21
138
  static deleteFile(filePath) {
22
- return this.defaultDataset.deleteFile(filePath);
139
+ return this.defaultStore.deleteFile(filePath);
23
140
  }
24
141
  static fileExists(filePath) {
25
- return this.defaultDataset.fileExists(filePath);
142
+ return this.defaultStore.fileExists(filePath);
26
143
  }
27
144
  static getFile(filePath) {
28
- return this.defaultDataset.getFile(filePath);
145
+ return this.defaultStore.getFile(filePath);
29
146
  }
30
147
  static listFiles(prefix) {
31
- return this.defaultDataset.listFiles(prefix);
148
+ return this.defaultStore.listFiles(prefix);
32
149
  }
33
- static saveFile(filePath, fileContent, mimeType, preventDuplicates = false) {
34
- return this.defaultDataset.saveFile(filePath, fileContent, mimeType, preventDuplicates);
150
+ static saveFile(filePath, fileContent, options,
151
+ /**
152
+ * Forwarded exactly as given. No default here on purpose: "unspecified"
153
+ * must reach the store so it can apply its own default.
154
+ */
155
+ preventDuplicates) {
156
+ return this.defaultStore.saveFile(filePath, fileContent, options, preventDuplicates);
35
157
  }
158
+ /**
159
+ * Clears the module-global registry and default store.
160
+ * For tests only — the registry is process-wide state.
161
+ *
162
+ * @internal
163
+ */
164
+ static resetForTests() {
165
+ this.stores.clear();
166
+ this.purposes.clear();
167
+ this.loggedFallbacks.clear();
168
+ this.defaultStore = undefined;
169
+ this.url = undefined;
170
+ registerWellKnownPurposes();
171
+ }
172
+ }
173
+ /** Purpose → the store configured for it. */
174
+ LinkedFileStorage.stores = new Map();
175
+ /** Every declared purpose, configured or not. */
176
+ LinkedFileStorage.purposes = new Map();
177
+ /** Purposes already reported as falling back, so the log fires once each. */
178
+ LinkedFileStorage.loggedFallbacks = new Set();
179
+ /** The purposes core ships with are declared at module load. */
180
+ function registerWellKnownPurposes() {
181
+ LinkedFileStorage.registerPurpose(FileStorePurposes.uploads, {
182
+ description: 'User-generated files. Falls back to the default store.',
183
+ });
184
+ LinkedFileStorage.registerPurpose(FileStorePurposes.appAssets, {
185
+ description: 'Built app bundles published by `linked build-app`.',
186
+ });
36
187
  }
188
+ registerWellKnownPurposes();
37
189
  /**
38
190
  * Get the full path of an asset based on the way LinkedFileStorage is configured
39
191
  * Returns accessURL + directory (/public by default) + path
@@ -1 +1 @@
1
- {"version":3,"file":"LinkedFileStorage.js","sourceRoot":"","sources":["../../../src/utils/LinkedFileStorage.ts"],"names":[],"mappings":"AAGA,MAAM,OAAgB,iBAAiB;IAIrC,MAAM,KAAK,SAAS;QAClB,8DAA8D;QAC9D,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,GAAG,CAAC;QAClB,CAAC;QAED,OAAO,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC;IACvC,CAAC;IAED,MAAM,CAAC,mBAAmB,CAAC,SAAiB;QAC1C,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;IAChC,CAAC;IAED,MAAM,CAAC,iBAAiB;QACtB,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED,MAAM,CAAC,iBAAiB,CAAC,OAAmB;QAC1C,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC;QAE9B,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YAC7B,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,MAAM,CAAC,UAAU,CAAC,QAAgB;QAChC,OAAO,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,CAAC,UAAU,CAAC,QAAgB;QAChC,OAAO,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,CAAC,OAAO,CAAC,QAAgB;QAC7B,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,CAAC,SAAS,CAAC,MAAe;QAC9B,OAAO,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,CAAC,QAAQ,CACb,QAAgB,EAChB,WAAoD,EACpD,QAAiB,EACjB,oBAA6B,KAAK;QAElC,OAAO,IAAI,CAAC,cAAc,CAAC,QAAQ,CACjC,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,iBAAiB,CAClB,CAAC;IACJ,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CAAC,IAAY,EAAE,YAAoB,SAAS;IAC/D,2EAA2E;IAC3E,sEAAsE;IACtE,wEAAwE;IACxE,0EAA0E;IAC1E,6CAA6C;IAC7C,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5F,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,iBAAiB,CAAC,SAAS,CAAC;IAC9C,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;IAC9C,OAAO,QAAQ,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"LinkedFileStorage.js","sourceRoot":"","sources":["../../../src/utils/LinkedFileStorage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@_linked/core",
3
- "version": "2.19.0",
3
+ "version": "2.20.1",
4
+ "packageManager": "npm@11.19.1",
4
5
  "license": "MIT",
5
6
  "linkedPackage": true,
6
7
  "type": "module",
7
8
  "description": "Linked.js core query and SHACL shape DSL (copy-then-prune baseline)",
8
9
  "repository": {
9
10
  "type": "git",
10
- "url": "git+https://github.com/linked-cm/core.git"
11
+ "url": "git+https://github.com/linked-fw/core.git"
11
12
  },
12
13
  "main": "lib/esm/index.js",
13
14
  "module": "lib/esm/index.js",