@endora-commerce/mod-assets-library 0.0.0-stage → 0.100.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.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/admin/api/assets-library-client.d.ts +29 -0
- package/dist/admin/api/assets-library-client.d.ts.map +1 -0
- package/dist/admin/api/assets-library-client.js +70 -0
- package/dist/admin/api/assets-library-client.js.map +1 -0
- package/dist/admin/components/AssetDetailDrawer.d.ts +8 -0
- package/dist/admin/components/AssetDetailDrawer.d.ts.map +1 -0
- package/dist/admin/components/AssetDetailDrawer.js +96 -0
- package/dist/admin/components/AssetDetailDrawer.js.map +1 -0
- package/dist/admin/components/FolderTree.d.ts +13 -0
- package/dist/admin/components/FolderTree.d.ts.map +1 -0
- package/dist/admin/components/FolderTree.js +71 -0
- package/dist/admin/components/FolderTree.js.map +1 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +46 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/LibraryPage.d.ts +9 -0
- package/dist/admin/pages/LibraryPage.d.ts.map +1 -0
- package/dist/admin/pages/LibraryPage.js +163 -0
- package/dist/admin/pages/LibraryPage.js.map +1 -0
- package/dist/backend/entities/asset-folder.entity.d.ts +16 -0
- package/dist/backend/entities/asset-folder.entity.d.ts.map +1 -0
- package/dist/backend/entities/asset-folder.entity.js +57 -0
- package/dist/backend/entities/asset-folder.entity.js.map +1 -0
- package/dist/backend/entities/asset.entity.d.ts +38 -0
- package/dist/backend/entities/asset.entity.d.ts.map +1 -0
- package/dist/backend/entities/asset.entity.js +128 -0
- package/dist/backend/entities/asset.entity.js.map +1 -0
- package/dist/backend/index.d.ts +57 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +150 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/jobs/hard-delete-asset.job.d.ts +18 -0
- package/dist/backend/jobs/hard-delete-asset.job.d.ts.map +1 -0
- package/dist/backend/jobs/hard-delete-asset.job.js +91 -0
- package/dist/backend/jobs/hard-delete-asset.job.js.map +1 -0
- package/dist/backend/plugin.d.ts +47 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +96 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +15 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +189 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.public.d.ts +14 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +99 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/asset-read-port.d.ts +103 -0
- package/dist/backend/services/asset-read-port.d.ts.map +1 -0
- package/dist/backend/services/asset-read-port.js +167 -0
- package/dist/backend/services/asset-read-port.js.map +1 -0
- package/dist/backend/services/assets-library.service.d.ts +76 -0
- package/dist/backend/services/assets-library.service.d.ts.map +1 -0
- package/dist/backend/services/assets-library.service.js +261 -0
- package/dist/backend/services/assets-library.service.js.map +1 -0
- package/dist/backend/services/cms-body-resolver.d.ts +6 -0
- package/dist/backend/services/cms-body-resolver.d.ts.map +1 -0
- package/dist/backend/services/cms-body-resolver.js +44 -0
- package/dist/backend/services/cms-body-resolver.js.map +1 -0
- package/dist/backend/services/folders.service.d.ts +29 -0
- package/dist/backend/services/folders.service.d.ts.map +1 -0
- package/dist/backend/services/folders.service.js +231 -0
- package/dist/backend/services/folders.service.js.map +1 -0
- package/dist/backend/services/hmac.d.ts +28 -0
- package/dist/backend/services/hmac.d.ts.map +1 -0
- package/dist/backend/services/hmac.js +50 -0
- package/dist/backend/services/hmac.js.map +1 -0
- package/dist/backend/services/reference-registry.d.ts +36 -0
- package/dist/backend/services/reference-registry.d.ts.map +1 -0
- package/dist/backend/services/reference-registry.js +63 -0
- package/dist/backend/services/reference-registry.js.map +1 -0
- package/dist/backend/services/storage/adapter-registry.d.ts +77 -0
- package/dist/backend/services/storage/adapter-registry.d.ts.map +1 -0
- package/dist/backend/services/storage/adapter-registry.js +95 -0
- package/dist/backend/services/storage/adapter-registry.js.map +1 -0
- package/dist/backend/services/storage/errors.d.ts +18 -0
- package/dist/backend/services/storage/errors.d.ts.map +1 -0
- package/dist/backend/services/storage/errors.js +30 -0
- package/dist/backend/services/storage/errors.js.map +1 -0
- package/dist/backend/services/storage/gcs-adapter.d.ts +42 -0
- package/dist/backend/services/storage/gcs-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/gcs-adapter.js +106 -0
- package/dist/backend/services/storage/gcs-adapter.js.map +1 -0
- package/dist/backend/services/storage/legacy-resolver.d.ts +15 -0
- package/dist/backend/services/storage/legacy-resolver.d.ts.map +1 -0
- package/dist/backend/services/storage/legacy-resolver.js +32 -0
- package/dist/backend/services/storage/legacy-resolver.js.map +1 -0
- package/dist/backend/services/storage/local-fs-adapter.d.ts +59 -0
- package/dist/backend/services/storage/local-fs-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/local-fs-adapter.js +153 -0
- package/dist/backend/services/storage/local-fs-adapter.js.map +1 -0
- package/dist/backend/services/storage/locator.d.ts +5 -0
- package/dist/backend/services/storage/locator.d.ts.map +1 -0
- package/dist/backend/services/storage/locator.js +16 -0
- package/dist/backend/services/storage/locator.js.map +1 -0
- package/dist/backend/services/storage/object-storage-port.d.ts +46 -0
- package/dist/backend/services/storage/object-storage-port.d.ts.map +1 -0
- package/dist/backend/services/storage/object-storage-port.js +78 -0
- package/dist/backend/services/storage/object-storage-port.js.map +1 -0
- package/dist/backend/services/storage/public-url-base.d.ts +50 -0
- package/dist/backend/services/storage/public-url-base.d.ts.map +1 -0
- package/dist/backend/services/storage/public-url-base.js +70 -0
- package/dist/backend/services/storage/public-url-base.js.map +1 -0
- package/dist/backend/services/storage/s3-adapter.d.ts +50 -0
- package/dist/backend/services/storage/s3-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/s3-adapter.js +110 -0
- package/dist/backend/services/storage/s3-adapter.js.map +1 -0
- package/dist/backend/services/storage/settings-view.d.ts +4 -0
- package/dist/backend/services/storage/settings-view.d.ts.map +1 -0
- package/dist/backend/services/storage/settings-view.js +73 -0
- package/dist/backend/services/storage/settings-view.js.map +1 -0
- package/dist/backend/services/storage/storage-adapter.d.ts +62 -0
- package/dist/backend/services/storage/storage-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/storage-adapter.js +10 -0
- package/dist/backend/services/storage/storage-adapter.js.map +1 -0
- package/dist/backend/services/upload-pipeline.d.ts +41 -0
- package/dist/backend/services/upload-pipeline.d.ts.map +1 -0
- package/dist/backend/services/upload-pipeline.js +231 -0
- package/dist/backend/services/upload-pipeline.js.map +1 -0
- package/dist/manifest.d.ts +185 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +229 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260505T102206_assets_library_init.d.ts +35 -0
- package/dist/migrations/20260505T102206_assets_library_init.d.ts.map +1 -0
- package/dist/migrations/20260505T102206_assets_library_init.js +132 -0
- package/dist/migrations/20260505T102206_assets_library_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/assets-library/index.md +125 -0
- package/i18n/en.json +54 -0
- package/i18n/pl.json +54 -0
- package/package.json +99 -3
- package/tailwind.css +14 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { resolvePublicApiBaseUrl } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { assetsLibraryModule } from './plugin.js';
|
|
3
|
+
import { AssetReadService } from './services/asset-read-port.js';
|
|
4
|
+
import { ObjectStorageAdapter } from './services/storage/object-storage-port.js';
|
|
5
|
+
import { AssetFolder } from './entities/asset-folder.entity.js';
|
|
6
|
+
import { Asset } from './entities/asset.entity.js';
|
|
7
|
+
export function registerModule(ctx) {
|
|
8
|
+
ctx.di.register({
|
|
9
|
+
assetsLibrary: ctx
|
|
10
|
+
.asFunction(({ emFactory, auditLogService }) => assetsLibraryModule({
|
|
11
|
+
emFactory,
|
|
12
|
+
// D-223 — this module resolves the deployment's public API origin
|
|
13
|
+
// itself, the idiom `payu`, `tpay` and `inpost` already use for their
|
|
14
|
+
// callback origins, and every URL it produces is built on it when the
|
|
15
|
+
// storage adapter's own configured base is blank. It is read here,
|
|
16
|
+
// inside the factory, so the value is the one this composition boots
|
|
17
|
+
// with; the platform has already refused a production boot that has
|
|
18
|
+
// no origin at all (issue #218), so there is no new failure mode.
|
|
19
|
+
publicApiBaseUrl: resolvePublicApiBaseUrl(),
|
|
20
|
+
// Resolved per check, not captured. Awilix's strict mode refuses a
|
|
21
|
+
// singleton holding the transient `requireAdmin` port, and it is
|
|
22
|
+
// right to: a captured guard keeps admitting requests after `auth`
|
|
23
|
+
// goes away. This indirection is what the gate being non-optional
|
|
24
|
+
// buys — there is one guard, and it is always the live one.
|
|
25
|
+
requireAdmin: (permission) => async (req, reply) => ctx.cradle().requireAdmin(permission)(req, reply),
|
|
26
|
+
auditLog: auditLogService,
|
|
27
|
+
}))
|
|
28
|
+
.singleton(),
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* The contribution seam, deliberately **ungated** (feature 072, D-39).
|
|
32
|
+
*
|
|
33
|
+
* `catalog`, `cms`, `blog` and `megamenu` push their reference scanners in
|
|
34
|
+
* from `ctx.onBoot`, and boot hooks run regardless of effective state. As a
|
|
35
|
+
* `providePort` this was a transient gate, so switching `assets_library` off
|
|
36
|
+
* made all four hooks throw `MODULE_DISABLED` during composition and the
|
|
37
|
+
* backend stopped starting. A descriptor is inert; `assetsLibraryPort`, which
|
|
38
|
+
* reads and writes assets, is the port and does fail closed. (That sentence
|
|
39
|
+
* named `assetsLibraryService` until T118c, which is the container name the
|
|
40
|
+
* roots contributed rather than a port — it was never gated, and it is gone.)
|
|
41
|
+
*
|
|
42
|
+
* Whether an entry is honoured while its contributor is absent is answered at
|
|
43
|
+
* enumeration — see `services/reference-registry.ts`.
|
|
44
|
+
*
|
|
45
|
+
* `AssetReferenceRegistryPort` is published over this name all the same, which
|
|
46
|
+
* is not a reason to convert it back (issue #192): a published shape and a
|
|
47
|
+
* gated registration are different questions, and only the first is what F4
|
|
48
|
+
* packages.
|
|
49
|
+
*/
|
|
50
|
+
ctx.di.register({
|
|
51
|
+
assetReferenceRegistry: ctx
|
|
52
|
+
.asFunction(({ assetsLibrary }) => assetsLibrary.handle.referenceRegistry)
|
|
53
|
+
.singleton(),
|
|
54
|
+
});
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Feature 075, Phase P — the published surface.
|
|
57
|
+
//
|
|
58
|
+
// `assetReadPort` replaces the five `em.findOne(Asset, …)` calls `catalog`
|
|
59
|
+
// and `orders` make to validate an id an admin supplied before attaching it.
|
|
60
|
+
//
|
|
61
|
+
// `assetsLibraryPort` is the narrow face of the library service that
|
|
62
|
+
// `pim_ergonode` uses during an import — upload, read, patch the alternate
|
|
63
|
+
// text, soft-delete what an item stopped pointing at. Since T118c it is also
|
|
64
|
+
// what `cms` resolves to turn an embedded asset id into the detail a
|
|
65
|
+
// storefront response carries.
|
|
66
|
+
//
|
|
67
|
+
// **Phase C has retired the root's entry** (`specs/110-instance-repository/`
|
|
68
|
+
// T118c). This note used to read *"the existing `assetsLibraryService` name is
|
|
69
|
+
// still contributed by a composition root; the root's entry stays until Phase
|
|
70
|
+
// C retires it, and the two resolve the same instance"*. Both roots
|
|
71
|
+
// contributed that name and, by the time it was drained, **no module resolved
|
|
72
|
+
// it**: `pim_ergonode` moved to this port at feature 075's cut and the second,
|
|
73
|
+
// ungated hold on the same service stayed behind. So the drain cost nothing
|
|
74
|
+
// and bought the one thing the duplicate could not have — there is now exactly
|
|
75
|
+
// one way in, and it is gated.
|
|
76
|
+
//
|
|
77
|
+
// What is still a composition's, and why it is not an omission. This note
|
|
78
|
+
// read *"five reaches … `resolveUrl` (three of them, two of those also
|
|
79
|
+
// folding in the host's public API base URL)"* against a tree holding
|
|
80
|
+
// **eight** reaches, **four** of them `resolveUrl` and **three** of those
|
|
81
|
+
// absolutizing. The count is not written down again (D-100):
|
|
82
|
+
// `grep -n 'assetsLibrary\.' backend/src/composition.ts` answers it. What is
|
|
83
|
+
// true of the reaches that remain is that each needs something this port does
|
|
84
|
+
// not publish — `resolveUrl`, or the storage adapters' byte-streaming surface
|
|
85
|
+
// (`invoices`' logo embed, `product_feeds`' artefact writer).
|
|
86
|
+
//
|
|
87
|
+
// **What no reach does any more is absolutize** (D-223). The three that did
|
|
88
|
+
// did it three ways — `absolutizePublicUrl` reading `BACKEND_PUBLIC_URL`
|
|
89
|
+
// first at two of them, `configuredPublicApiBaseUrl` plus a hand-written join
|
|
90
|
+
// reading `PUBLIC_API_BASE_URL` first at the third — while the fourth
|
|
91
|
+
// `resolveUrl` reach did not absolutize at all, so one asset had three
|
|
92
|
+
// answers depending on which consumer asked, and the two composition roots
|
|
93
|
+
// disagreed at two of the sites. This module resolves the origin itself now
|
|
94
|
+
// and every URL it returns is absolute.
|
|
95
|
+
//
|
|
96
|
+
// **`objectStoragePort` is the byte-store answer** (T118c), and it is a
|
|
97
|
+
// sibling of `assetsLibraryPort` rather than four more methods on it: what
|
|
98
|
+
// `product_feeds` borrows is *which bucket this deployment writes to, with
|
|
99
|
+
// which credentials*, and it creates no `Asset` row. `invoices`' logo embed
|
|
100
|
+
// asks the identical question through a composition root and drains onto the
|
|
101
|
+
// same port. The published shape's own doc block carries its retiring
|
|
102
|
+
// condition — this is really an object store, and it lives here because the
|
|
103
|
+
// configuration does.
|
|
104
|
+
//
|
|
105
|
+
// **And the URL answer is `assetReadPort.resolvePublicUrls`, in batch.** This
|
|
106
|
+
// note read *"a consumer that wants a URL for an asset id takes
|
|
107
|
+
// `AssetsLibraryPort.getAsset`, which already carries one"*, and that is the
|
|
108
|
+
// right answer for `cms` resolving a handful of embeds on one page. Measured
|
|
109
|
+
// for a consumer that asks about hundreds at a time, it is not: `getAsset`
|
|
110
|
+
// builds the full detail, and the detail carries the deletion-protection
|
|
111
|
+
// `references` list, which is one query per registered reference descriptor —
|
|
112
|
+
// roughly ten queries per asset against a feed hydration batch of 500
|
|
113
|
+
// products. The batched read is one query and a string build per row, and it
|
|
114
|
+
// is the *rule* that moves with it rather than only the loop: only this
|
|
115
|
+
// module can tell a stable URL from a signed one, so the filter belongs here
|
|
116
|
+
// and the absence is the return type (composition checklist item 7).
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
ctx.di.providePort('assetReadPort', ctx
|
|
119
|
+
.asFunction(({ emFactory, assetsLibrary }) =>
|
|
120
|
+
// The registry is read per call, never captured: it is rebuilt when
|
|
121
|
+
// the active-backend setting changes, and a URL built by yesterday's
|
|
122
|
+
// adapter points at a bucket this deployment no longer writes to.
|
|
123
|
+
new AssetReadService(emFactory, () => assetsLibrary.handle.adapters))
|
|
124
|
+
.singleton());
|
|
125
|
+
ctx.di.providePort('assetsLibraryPort', ctx
|
|
126
|
+
.asFunction(({ assetsLibrary }) => assetsLibrary.handle.service)
|
|
127
|
+
.singleton());
|
|
128
|
+
ctx.di.providePort('objectStoragePort', ctx
|
|
129
|
+
.asFunction(({ assetsLibrary }) => new ObjectStorageAdapter(assetsLibrary.handle.adapters))
|
|
130
|
+
.singleton());
|
|
131
|
+
ctx.routes(async (app) => {
|
|
132
|
+
await ctx.cradle().assetsLibrary.plugin(app);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
137
|
+
* array and **no named class export** (D-168).
|
|
138
|
+
*
|
|
139
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
140
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
141
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
142
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
143
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
144
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
145
|
+
*/
|
|
146
|
+
export const entities = [
|
|
147
|
+
AssetFolder,
|
|
148
|
+
Asset,
|
|
149
|
+
];
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { AdapterRegistry } from '../services/storage/adapter-registry.js';
|
|
3
|
+
export interface HardDeleteSweepResult {
|
|
4
|
+
hardDeleted: number;
|
|
5
|
+
pendingCleanup: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class HardDeleteAssetWorker {
|
|
8
|
+
private readonly emFactory;
|
|
9
|
+
private readonly adapters;
|
|
10
|
+
constructor(emFactory: () => EntityManager, adapters: AdapterRegistry);
|
|
11
|
+
sweep(now?: Date): Promise<HardDeleteSweepResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Retry pass for rows the worker previously parked in pendingCleanup.
|
|
14
|
+
* Same logic as `sweep` but selects rows with pending_cleanup = true.
|
|
15
|
+
*/
|
|
16
|
+
sweepPending(): Promise<HardDeleteSweepResult>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=hard-delete-asset.job.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hard-delete-asset.job.d.ts","sourceRoot":"","sources":["../../../src/backend/jobs/hard-delete-asset.job.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAG/E,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,eAAe;IAGtC,KAAK,CAAC,GAAG,GAAE,IAAiB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAoCnE;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,qBAAqB,CAAC;CA8BrD"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Hard-delete worker — feature 013 / FR-031 / Phase 9 / T100.
|
|
2
|
+
//
|
|
3
|
+
// Periodic sweep that finalises soft-deleted assets:
|
|
4
|
+
// 1. Find every Asset where deletedAt is set, purgeAfterAt has elapsed,
|
|
5
|
+
// and pendingCleanup is false.
|
|
6
|
+
// 2. For each, call adapter.delete(locator). Success → DELETE the row.
|
|
7
|
+
// Failure → set pendingCleanup = true; the row stays soft-deleted and
|
|
8
|
+
// the worker retries on the next tick.
|
|
9
|
+
//
|
|
10
|
+
// `sweep()` is a plain async method so tests can drive it directly without
|
|
11
|
+
// Redis/BullMQ. Production wiring registers it on the existing repeatable
|
|
12
|
+
// queue (omitted here — repeatable scheduling lives in composition.ts).
|
|
13
|
+
import { Asset } from '../entities/asset.entity.js';
|
|
14
|
+
export class HardDeleteAssetWorker {
|
|
15
|
+
emFactory;
|
|
16
|
+
adapters;
|
|
17
|
+
constructor(emFactory, adapters) {
|
|
18
|
+
this.emFactory = emFactory;
|
|
19
|
+
this.adapters = adapters;
|
|
20
|
+
}
|
|
21
|
+
async sweep(now = new Date()) {
|
|
22
|
+
// command-coverage-ignore: retention purge, not an operator write. The
|
|
23
|
+
// auditable decision was the soft delete that set `deletedAt` and
|
|
24
|
+
// `purgeAfterAt`; this pass only executes the retention window that write
|
|
25
|
+
// already committed, with no actor behind it and nothing to undo.
|
|
26
|
+
const em = this.emFactory();
|
|
27
|
+
const due = await em.find(Asset, {
|
|
28
|
+
deletedAt: { $ne: null },
|
|
29
|
+
purgeAfterAt: { $lte: now },
|
|
30
|
+
pendingCleanup: false,
|
|
31
|
+
});
|
|
32
|
+
let hardDeleted = 0;
|
|
33
|
+
let newlyPending = 0;
|
|
34
|
+
for (const a of due) {
|
|
35
|
+
const adapter = await this.adapters.getForBackend(a.storageBackend);
|
|
36
|
+
// Legacy resolver has no delete — skip and just hard-delete the row.
|
|
37
|
+
if (!('delete' in adapter) || typeof adapter.delete !== 'function') {
|
|
38
|
+
em.remove(a);
|
|
39
|
+
hardDeleted += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await adapter.delete({ locator: a.storageLocator || a.storageUrl });
|
|
44
|
+
em.remove(a);
|
|
45
|
+
hardDeleted += 1;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
a.pendingCleanup = true;
|
|
49
|
+
newlyPending += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (due.length > 0)
|
|
53
|
+
await em.flush();
|
|
54
|
+
return { hardDeleted, pendingCleanup: newlyPending };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Retry pass for rows the worker previously parked in pendingCleanup.
|
|
58
|
+
* Same logic as `sweep` but selects rows with pending_cleanup = true.
|
|
59
|
+
*/
|
|
60
|
+
async sweepPending() {
|
|
61
|
+
// command-coverage-ignore: the retry half of `sweep()` — same already-audited
|
|
62
|
+
// retention decision, re-attempted for rows whose storage delete failed.
|
|
63
|
+
const em = this.emFactory();
|
|
64
|
+
const due = await em.find(Asset, {
|
|
65
|
+
deletedAt: { $ne: null },
|
|
66
|
+
pendingCleanup: true,
|
|
67
|
+
});
|
|
68
|
+
let hardDeleted = 0;
|
|
69
|
+
let stillPending = 0;
|
|
70
|
+
for (const a of due) {
|
|
71
|
+
const adapter = await this.adapters.getForBackend(a.storageBackend);
|
|
72
|
+
if (!('delete' in adapter) || typeof adapter.delete !== 'function') {
|
|
73
|
+
em.remove(a);
|
|
74
|
+
hardDeleted += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await adapter.delete({ locator: a.storageLocator || a.storageUrl });
|
|
79
|
+
em.remove(a);
|
|
80
|
+
hardDeleted += 1;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
stillPending += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (due.length > 0)
|
|
87
|
+
await em.flush();
|
|
88
|
+
return { hardDeleted, pendingCleanup: stillPending };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=hard-delete-asset.job.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hard-delete-asset.job.js","sourceRoot":"","sources":["../../../src/backend/jobs/hard-delete-asset.job.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
import { AdapterRegistry } from './services/storage/adapter-registry.js';
|
|
4
|
+
import { AssetsLibraryService } from './services/assets-library.service.js';
|
|
5
|
+
import { FoldersService } from './services/folders.service.js';
|
|
6
|
+
import { AssetReferenceRegistry } from './services/reference-registry.js';
|
|
7
|
+
import { HmacSigner } from './services/hmac.js';
|
|
8
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
9
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
10
|
+
export interface AssetsLibraryModuleOptions {
|
|
11
|
+
emFactory: () => EntityManager;
|
|
12
|
+
/** HMAC signer for local-FS private URLs. Uses ASSETS_LIBRARY_HMAC_KEY by default. */
|
|
13
|
+
signer?: HmacSigner;
|
|
14
|
+
/**
|
|
15
|
+
* Permission gate factory. **Required** since feature 072 (T092): it used to
|
|
16
|
+
* default to a permissive no-op, so a caller that forgot it got an
|
|
17
|
+
* unguarded asset admin surface — silently, because the safe path is the one
|
|
18
|
+
* every test exercises. The container supplies it now; there is no omission
|
|
19
|
+
* left to be silent about.
|
|
20
|
+
*/
|
|
21
|
+
requireAdmin: RequireAdminFactory;
|
|
22
|
+
/**
|
|
23
|
+
* This deployment's resolved public API origin, and the base every URL this
|
|
24
|
+
* module produces falls back to (D-223).
|
|
25
|
+
*
|
|
26
|
+
* **Required**, for the reason the gate above is: an omitted origin is not a
|
|
27
|
+
* failure, it is a host-relative URL in an e-mail, a push payload and a
|
|
28
|
+
* partner's feed — silent, and invisible to every test that reads the URL
|
|
29
|
+
* from the same host that served it. `registerModule` resolves it with the
|
|
30
|
+
* platform's `resolvePublicApiBaseUrl()`, which is the idiom the payment
|
|
31
|
+
* modules already use for their callback origins.
|
|
32
|
+
*/
|
|
33
|
+
publicApiBaseUrl: string;
|
|
34
|
+
/** Feature 054 — audits asset/folder writes co-transactionally when provided. */
|
|
35
|
+
auditLog?: AuditPort;
|
|
36
|
+
}
|
|
37
|
+
export interface AssetsLibraryModuleHandle {
|
|
38
|
+
service: AssetsLibraryService;
|
|
39
|
+
folders: FoldersService;
|
|
40
|
+
referenceRegistry: AssetReferenceRegistry;
|
|
41
|
+
adapters: AdapterRegistry;
|
|
42
|
+
}
|
|
43
|
+
export declare function assetsLibraryModule(options: AssetsLibraryModuleOptions): {
|
|
44
|
+
plugin: (app: FastifyInstance) => Promise<void>;
|
|
45
|
+
handle: AssetsLibraryModuleHandle;
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAMlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,sFAAsF;IACtF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;;OAMG;IACH,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAyBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG;IACxE,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,EAAE,yBAAyB,CAAC;CACnC,CAqEA"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Assets Library — Fastify plugin / composition root for the module.
|
|
2
|
+
import fastifyMultipart from '@fastify/multipart';
|
|
3
|
+
import { AdapterRegistry } from './services/storage/adapter-registry.js';
|
|
4
|
+
import { AssetsLibraryService } from './services/assets-library.service.js';
|
|
5
|
+
import { FoldersService } from './services/folders.service.js';
|
|
6
|
+
import { AssetReferenceRegistry } from './services/reference-registry.js';
|
|
7
|
+
import { HmacSigner } from './services/hmac.js';
|
|
8
|
+
import { createSettingsView } from './services/storage/settings-view.js';
|
|
9
|
+
import { Setting } from '@endora-commerce/platform/kernel';
|
|
10
|
+
import { SettingValue } from '@endora-commerce/platform/kernel';
|
|
11
|
+
import { registerAssetsLibraryAdminRoutes } from './routes.admin.js';
|
|
12
|
+
import { registerAssetsLibraryPublicRoutes } from './routes.public.js';
|
|
13
|
+
async function loadUploadPolicy(emFactory) {
|
|
14
|
+
const em = emFactory();
|
|
15
|
+
const allowed = await em.findOne(Setting, { code: 'assets.allowed_file_types' });
|
|
16
|
+
const allowedOverride = allowed
|
|
17
|
+
? await em.findOne(SettingValue, { setting: allowed })
|
|
18
|
+
: null;
|
|
19
|
+
const allowedRaw = allowedOverride
|
|
20
|
+
? allowedOverride.value
|
|
21
|
+
: allowed?.defaultValue ?? ['*'];
|
|
22
|
+
const allowedTypes = Array.isArray(allowedRaw) ? allowedRaw.map(String) : ['*'];
|
|
23
|
+
const max = await em.findOne(Setting, { code: 'assets.max_file_size_mb' });
|
|
24
|
+
const maxOverride = max ? await em.findOne(SettingValue, { setting: max }) : null;
|
|
25
|
+
const maxRaw = maxOverride ? maxOverride.value : max?.defaultValue ?? 0;
|
|
26
|
+
const maxFileSizeMb = typeof maxRaw === 'number' ? maxRaw : Number(maxRaw) || 0;
|
|
27
|
+
return { allowedTypes, maxFileSizeMb };
|
|
28
|
+
}
|
|
29
|
+
export function assetsLibraryModule(options) {
|
|
30
|
+
let cachedSigner = options.signer;
|
|
31
|
+
const adapters = new AdapterRegistry({
|
|
32
|
+
settings: createSettingsView(options.emFactory),
|
|
33
|
+
publicApiBaseUrl: options.publicApiBaseUrl,
|
|
34
|
+
signer: () => {
|
|
35
|
+
if (!cachedSigner)
|
|
36
|
+
cachedSigner = HmacSigner.fromEnv();
|
|
37
|
+
return cachedSigner;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const referenceRegistry = new AssetReferenceRegistry();
|
|
41
|
+
const service = new AssetsLibraryService({
|
|
42
|
+
emFactory: options.emFactory,
|
|
43
|
+
adapters,
|
|
44
|
+
referenceRegistry,
|
|
45
|
+
loadUploadPolicy: () => loadUploadPolicy(options.emFactory),
|
|
46
|
+
...(options.auditLog ? { auditLog: options.auditLog } : {}),
|
|
47
|
+
});
|
|
48
|
+
const folders = new FoldersService(options.emFactory, referenceRegistry, options.auditLog);
|
|
49
|
+
const requireAdmin = options.requireAdmin;
|
|
50
|
+
const plugin = async (app) => {
|
|
51
|
+
// Resolve the upload policy ONCE at registration so the multipart
|
|
52
|
+
// parser's hard cap matches the operator-configured maximum. Without
|
|
53
|
+
// an explicit `fileSize`, @fastify/multipart silently falls back to
|
|
54
|
+
// its 1 MiB default and tears down the file stream on anything larger
|
|
55
|
+
// — which surfaces as a 500 from the awaited adapter pipeline rather
|
|
56
|
+
// than a clean 413 (the symptom that motivated this code).
|
|
57
|
+
//
|
|
58
|
+
// The pipeline's per-request size check still runs against the LIVE
|
|
59
|
+
// setting on every upload, so tightening the cap takes effect without
|
|
60
|
+
// a restart. Loosening it (or switching from a finite cap to "no cap")
|
|
61
|
+
// requires a backend restart for the multipart parser to pick up the
|
|
62
|
+
// new `fileSize` value.
|
|
63
|
+
const initialPolicy = await loadUploadPolicy(options.emFactory);
|
|
64
|
+
const FALLBACK_HARD_CEILING_BYTES = 5 * 1024 * 1024 * 1024; // 5 GiB
|
|
65
|
+
const multipartFileSize = initialPolicy.maxFileSizeMb > 0
|
|
66
|
+
? initialPolicy.maxFileSizeMb * 1024 * 1024
|
|
67
|
+
: FALLBACK_HARD_CEILING_BYTES;
|
|
68
|
+
// Multipart is registered in a child encapsulation context so other modules'
|
|
69
|
+
// routes are unaffected by its options.
|
|
70
|
+
await app.register(async (childApp) => {
|
|
71
|
+
await childApp.register(fastifyMultipart, {
|
|
72
|
+
// Keep memory bounded; upload-pipeline streams directly to the adapter.
|
|
73
|
+
limits: { files: 1, fields: 10, fileSize: multipartFileSize },
|
|
74
|
+
});
|
|
75
|
+
await registerAssetsLibraryAdminRoutes(childApp, {
|
|
76
|
+
service,
|
|
77
|
+
folders,
|
|
78
|
+
adapters,
|
|
79
|
+
emFactory: options.emFactory,
|
|
80
|
+
requireAdmin,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
await registerAssetsLibraryPublicRoutes(app, {
|
|
84
|
+
service,
|
|
85
|
+
adapters,
|
|
86
|
+
emFactory: options.emFactory,
|
|
87
|
+
signer: () => {
|
|
88
|
+
if (!cachedSigner)
|
|
89
|
+
cachedSigner = HmacSigner.fromEnv();
|
|
90
|
+
return cachedSigner;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
return { plugin, handle: { service, folders, referenceRegistry, adapters } };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAAA,qEAAqE;AAIrE,OAAO,gBAAgB,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,gCAAgC,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,iCAAiC,EAAE,MAAM,oBAAoB,CAAC;AAuCvE,KAAK,UAAU,gBAAgB,CAAC,SAA8B;IAI5D,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IACvB,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,2BAA2B,EAAE,CAAC,CAAC;IACjF,MAAM,eAAe,GAAG,OAAO;QAC7B,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QACtD,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,UAAU,GAAG,eAAe;QAChC,CAAC,CAAC,eAAe,CAAC,KAAK;QACvB,CAAC,CAAC,OAAO,EAAE,YAAY,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAEhF,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,yBAAyB,EAAE,CAAC,CAAC;IAC3E,MAAM,WAAW,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClF,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,YAAY,IAAI,CAAC,CAAC;IACxE,MAAM,aAAa,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEhF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAmC;IAIrE,IAAI,YAAY,GAA2B,OAAO,CAAC,MAAM,CAAC;IAC1D,MAAM,QAAQ,GAAG,IAAI,eAAe,CAAC;QACnC,QAAQ,EAAE,kBAAkB,CAAC,OAAO,CAAC,SAAS,CAAC;QAC/C,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;QAC1C,MAAM,EAAE,GAAG,EAAE;YACX,IAAI,CAAC,YAAY;gBAAE,YAAY,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC;YACvD,OAAO,YAAY,CAAC;QACtB,CAAC;KACF,CAAC,CAAC;IACH,MAAM,iBAAiB,GAAG,IAAI,sBAAsB,EAAE,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,oBAAoB,CAAC;QACvC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,QAAQ;QACR,iBAAiB;QACjB,gBAAgB,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC;QAC3D,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5D,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,SAAS,EAAE,iBAAiB,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE3F,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAE1C,MAAM,MAAM,GAAG,KAAK,EAAE,GAAoB,EAAE,EAAE;QAC5C,kEAAkE;QAClE,qEAAqE;QACrE,oEAAoE;QACpE,sEAAsE;QACtE,qEAAqE;QACrE,2DAA2D;QAC3D,EAAE;QACF,oEAAoE;QACpE,sEAAsE;QACtE,uEAAuE;QACvE,qEAAqE;QACrE,wBAAwB;QACxB,MAAM,aAAa,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAChE,MAAM,2BAA2B,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;QACpE,MAAM,iBAAiB,GACrB,aAAa,CAAC,aAAa,GAAG,CAAC;YAC7B,CAAC,CAAC,aAAa,CAAC,aAAa,GAAG,IAAI,GAAG,IAAI;YAC3C,CAAC,CAAC,2BAA2B,CAAC;QAElC,6EAA6E;QAC7E,wCAAwC;QACxC,MAAM,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;YACpC,MAAM,QAAQ,CAAC,QAAQ,CAAC,gBAAgB,EAAE;gBACxC,wEAAwE;gBACxE,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,iBAAiB,EAAE;aAC9D,CAAC,CAAC;YACH,MAAM,gCAAgC,CAAC,QAAQ,EAAE;gBAC/C,OAAO;gBACP,OAAO;gBACP,QAAQ;gBACR,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,YAAY;aACb,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,iCAAiC,CAAC,GAAG,EAAE;YAC3C,OAAO;YACP,QAAQ;YACR,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,MAAM,EAAE,GAAG,EAAE;gBACX,IAAI,CAAC,YAAY;oBAAE,YAAY,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC;gBACvD,OAAO,YAAY,CAAC;YACtB,CAAC;SACF,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
import type { AssetsLibraryService } from './services/assets-library.service.js';
|
|
4
|
+
import type { FoldersService } from './services/folders.service.js';
|
|
5
|
+
import type { AdapterRegistry } from './services/storage/adapter-registry.js';
|
|
6
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
7
|
+
export interface AdminRoutesDeps {
|
|
8
|
+
service: AssetsLibraryService;
|
|
9
|
+
folders: FoldersService;
|
|
10
|
+
adapters: AdapterRegistry;
|
|
11
|
+
emFactory: () => EntityManager;
|
|
12
|
+
requireAdmin: RequireAdminFactory;
|
|
13
|
+
}
|
|
14
|
+
export declare function registerAssetsLibraryAdminRoutes(app: FastifyInstance, deps: AdminRoutesDeps): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=routes.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAc3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAmQf"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Admin HTTP surface for the Assets Library — feature 013 / US1.
|
|
2
|
+
// Mounts under /api/v1/admin/assets/*.
|
|
3
|
+
//
|
|
4
|
+
// Routes implemented in this phase (US1):
|
|
5
|
+
// POST /api/v1/admin/assets — multipart upload
|
|
6
|
+
// GET /api/v1/admin/assets — list (folder filter + pagination)
|
|
7
|
+
// GET /api/v1/admin/assets/:id — detail with references
|
|
8
|
+
// GET /api/v1/admin/assets/:id/url — re-resolve URL on demand
|
|
9
|
+
// PATCH /api/v1/admin/assets/:id — edit metadata + visibility flip
|
|
10
|
+
//
|
|
11
|
+
// Folder CRUD, soft-delete, restore, bulk move, and storage self-check
|
|
12
|
+
// land in subsequent user stories.
|
|
13
|
+
import { ERROR_CODES, patchAssetRequestSchema, listAssetsQuerySchema, createFolderRequestSchema, patchFolderRequestSchema, deleteFolderRequestSchema, moveAssetRequestSchema, moveManyAssetsRequestSchema, } from '@endora-commerce/contracts';
|
|
14
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
15
|
+
import { Asset } from './entities/asset.entity.js';
|
|
16
|
+
export async function registerAssetsLibraryAdminRoutes(app, deps) {
|
|
17
|
+
const { service, folders, adapters, emFactory, requireAdmin } = deps;
|
|
18
|
+
// — POST /assets (multipart upload) -----------------------------------------
|
|
19
|
+
app.post('/api/v1/admin/assets', { preHandler: requireAdmin('assets.write') }, async (req, reply) => {
|
|
20
|
+
// Confirm we're handling a multipart request.
|
|
21
|
+
if (!req.isMultipart()) {
|
|
22
|
+
throw new HttpError(400, ERROR_CODES.ASSET_UPLOAD_NO_FILE, 'Upload requires multipart/form-data with a `file` part.');
|
|
23
|
+
}
|
|
24
|
+
// Collect non-file fields once; @fastify/multipart streams parts.
|
|
25
|
+
let folderId = null;
|
|
26
|
+
let label = null;
|
|
27
|
+
let visibility = 'public';
|
|
28
|
+
let asset = null;
|
|
29
|
+
for await (const part of req.parts()) {
|
|
30
|
+
if (part.type === 'file') {
|
|
31
|
+
if (asset) {
|
|
32
|
+
throw new HttpError(400, ERROR_CODES.ASSET_UPLOAD_NO_FILE, 'Only one file per request is allowed.');
|
|
33
|
+
}
|
|
34
|
+
asset = await service.upload({
|
|
35
|
+
filename: part.filename ?? 'unknown',
|
|
36
|
+
declaredMime: part.mimetype ?? 'application/octet-stream',
|
|
37
|
+
stream: part.file,
|
|
38
|
+
// @fastify/multipart does not provide a guaranteed size.
|
|
39
|
+
declaredSize: 0,
|
|
40
|
+
folderId,
|
|
41
|
+
label,
|
|
42
|
+
visibility,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (part.fieldname === 'folderId') {
|
|
46
|
+
folderId = part.value || null;
|
|
47
|
+
}
|
|
48
|
+
else if (part.fieldname === 'label') {
|
|
49
|
+
label = part.value || null;
|
|
50
|
+
}
|
|
51
|
+
else if (part.fieldname === 'visibility') {
|
|
52
|
+
const v = part.value;
|
|
53
|
+
if (v !== 'public' && v !== 'private') {
|
|
54
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `visibility must be public|private; got "${v}".`);
|
|
55
|
+
}
|
|
56
|
+
visibility = v;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!asset) {
|
|
60
|
+
throw new HttpError(400, ERROR_CODES.ASSET_UPLOAD_NO_FILE, 'Multipart body did not include a `file` part.');
|
|
61
|
+
}
|
|
62
|
+
reply.status(201);
|
|
63
|
+
return { data: asset };
|
|
64
|
+
});
|
|
65
|
+
// — GET /assets (paginated list) -------------------------------------------
|
|
66
|
+
app.get('/api/v1/admin/assets', {
|
|
67
|
+
preHandler: requireAdmin('assets.read'),
|
|
68
|
+
}, async (req) => {
|
|
69
|
+
const parsed = listAssetsQuerySchema.parse(req.query);
|
|
70
|
+
const out = await service.listAssets({
|
|
71
|
+
folderId: parsed.folderId ?? null,
|
|
72
|
+
...(parsed.q !== undefined ? { q: parsed.q } : {}),
|
|
73
|
+
...(parsed.mime !== undefined ? { mime: parsed.mime } : {}),
|
|
74
|
+
...(parsed.visibility !== undefined ? { visibility: parsed.visibility } : {}),
|
|
75
|
+
includeDeleted: parsed.includeDeleted,
|
|
76
|
+
...(parsed.cursor !== undefined ? { cursor: parsed.cursor } : {}),
|
|
77
|
+
limit: parsed.limit,
|
|
78
|
+
});
|
|
79
|
+
return out;
|
|
80
|
+
});
|
|
81
|
+
// — GET /assets/:id --------------------------------------------------------
|
|
82
|
+
app.get('/api/v1/admin/assets/:id', { preHandler: requireAdmin('assets.read') }, async (req) => {
|
|
83
|
+
const detail = await service.getAsset(req.params.id);
|
|
84
|
+
return { data: detail };
|
|
85
|
+
});
|
|
86
|
+
// — GET /assets/:id/url ----------------------------------------------------
|
|
87
|
+
app.get('/api/v1/admin/assets/:id/url', { preHandler: requireAdmin('assets.read') }, async (req) => {
|
|
88
|
+
const resolved = await service.resolveUrl(req.params.id);
|
|
89
|
+
return {
|
|
90
|
+
data: {
|
|
91
|
+
url: resolved.url,
|
|
92
|
+
expiresAt: resolved.expiresAt ? resolved.expiresAt.toISOString() : null,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
// — DELETE /assets/:id (soft-delete + reference protection) ---------------
|
|
97
|
+
app.delete('/api/v1/admin/assets/:id', { preHandler: requireAdmin('assets.write') }, async (req) => {
|
|
98
|
+
const out = await service.softDelete(req.params.id);
|
|
99
|
+
return {
|
|
100
|
+
data: {
|
|
101
|
+
deletedAt: out.deletedAt.toISOString(),
|
|
102
|
+
purgeAfterAt: out.purgeAfterAt.toISOString(),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
// — POST /assets/:id/restore ----------------------------------------------
|
|
107
|
+
app.post('/api/v1/admin/assets/:id/restore', { preHandler: requireAdmin('assets.write') }, async (req) => {
|
|
108
|
+
const detail = await service.restore(req.params.id);
|
|
109
|
+
return { data: detail };
|
|
110
|
+
});
|
|
111
|
+
// — POST /assets/:id/move --------------------------------------------------
|
|
112
|
+
app.post('/api/v1/admin/assets/:id/move', { preHandler: requireAdmin('assets.write') }, async (req) => {
|
|
113
|
+
const body = moveAssetRequestSchema.parse(req.body);
|
|
114
|
+
const detail = await service.move(req.params.id, body.folderId ?? null);
|
|
115
|
+
return { data: detail };
|
|
116
|
+
});
|
|
117
|
+
// — POST /assets/move-many -------------------------------------------------
|
|
118
|
+
app.post('/api/v1/admin/assets/move-many', { preHandler: requireAdmin('assets.write') }, async (req) => {
|
|
119
|
+
const body = moveManyAssetsRequestSchema.parse(req.body);
|
|
120
|
+
const out = await service.moveMany(body.assetIds, body.folderId ?? null);
|
|
121
|
+
return { data: out };
|
|
122
|
+
});
|
|
123
|
+
// — Storage administration -------------------------------------------------
|
|
124
|
+
app.post('/api/v1/admin/assets/storage/self-check', { preHandler: requireAdmin('assets.write') }, async () => {
|
|
125
|
+
const adapter = await adapters.getActive();
|
|
126
|
+
const result = await adapter.selfCheck();
|
|
127
|
+
return {
|
|
128
|
+
data: { adapter: adapter.code, ...result },
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
app.get('/api/v1/admin/assets/storage/state', { preHandler: requireAdmin('assets.read') }, async () => {
|
|
132
|
+
const adapter = await adapters.getActive().catch((err) => {
|
|
133
|
+
return {
|
|
134
|
+
code: 'local',
|
|
135
|
+
// Surface the misconfig as a not-ok self-check rather than 500.
|
|
136
|
+
async selfCheck() {
|
|
137
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
const result = await adapter.selfCheck();
|
|
142
|
+
const em = emFactory();
|
|
143
|
+
const pendingCleanupCount = await em.count(Asset, { pendingCleanup: true });
|
|
144
|
+
const softDeletedCount = await em.count(Asset, { deletedAt: { $ne: null } });
|
|
145
|
+
return {
|
|
146
|
+
data: {
|
|
147
|
+
activeAdapter: adapter.code,
|
|
148
|
+
selfCheck: result,
|
|
149
|
+
pendingCleanupCount,
|
|
150
|
+
softDeletedCount,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
// — Folders ----------------------------------------------------------------
|
|
155
|
+
app.get('/api/v1/admin/assets/folders', { preHandler: requireAdmin('assets.read') }, async () => ({ data: await folders.listFolderTree() }));
|
|
156
|
+
app.post('/api/v1/admin/assets/folders', { preHandler: requireAdmin('assets.write') }, async (req, reply) => {
|
|
157
|
+
const body = createFolderRequestSchema.parse(req.body);
|
|
158
|
+
const f = await folders.createFolder(body);
|
|
159
|
+
reply.status(201);
|
|
160
|
+
return { data: f };
|
|
161
|
+
});
|
|
162
|
+
app.patch('/api/v1/admin/assets/folders/:folderId', { preHandler: requireAdmin('assets.write') }, async (req) => {
|
|
163
|
+
const body = patchFolderRequestSchema.parse(req.body);
|
|
164
|
+
const f = await folders.patchFolder(req.params.folderId, {
|
|
165
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
166
|
+
...(body.parentId !== undefined ? { parentId: body.parentId } : {}),
|
|
167
|
+
...(body.position !== undefined ? { position: body.position } : {}),
|
|
168
|
+
});
|
|
169
|
+
return { data: f };
|
|
170
|
+
});
|
|
171
|
+
app.delete('/api/v1/admin/assets/folders/:folderId', { preHandler: requireAdmin('assets.write') }, async (req) => {
|
|
172
|
+
const body = deleteFolderRequestSchema.parse(req.body ?? {});
|
|
173
|
+
const out = await folders.deleteFolder(req.params.folderId, body.ifNonEmpty);
|
|
174
|
+
return { data: out };
|
|
175
|
+
});
|
|
176
|
+
// — PATCH /assets/:id ------------------------------------------------------
|
|
177
|
+
app.patch('/api/v1/admin/assets/:id', { preHandler: requireAdmin('assets.write') }, async (req) => {
|
|
178
|
+
const patch = patchAssetRequestSchema.parse(req.body);
|
|
179
|
+
const detail = await service.patchAsset(req.params.id, {
|
|
180
|
+
...(patch.filename !== undefined ? { filename: patch.filename } : {}),
|
|
181
|
+
...(patch.label !== undefined ? { label: patch.label } : {}),
|
|
182
|
+
...(patch.mimeType !== undefined ? { mimeType: patch.mimeType } : {}),
|
|
183
|
+
...(patch.visibility !== undefined ? { visibility: patch.visibility } : {}),
|
|
184
|
+
...(patch.folderId !== undefined ? { folderId: patch.folderId } : {}),
|
|
185
|
+
});
|
|
186
|
+
return { data: detail };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=routes.admin.js.map
|