@jini-ai/cms 0.4.1 → 0.4.2
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 +7 -0
- package/dist/media/blob-store.fs.d.ts +4 -0
- package/dist/media/blob-store.fs.js +27 -0
- package/dist/media/blob-store.memory.d.ts +3 -0
- package/dist/media/blob-store.memory.js +3 -0
- package/dist/media/created-by.d.ts +15 -0
- package/dist/media/created-by.js +20 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.js +1 -0
- package/dist/media/media-service.d.ts +1 -1
- package/dist/media/media-service.js +1 -0
- package/dist/media/ports.d.ts +12 -2
- package/dist/media/repo.memory.js +2 -1
- package/dist/media/tool-registrations.js +1 -0
- package/dist/media/types.d.ts +4 -0
- package/dist/navigation/menu-service.js +6 -0
- package/dist/navigation/types.d.ts +5 -0
- package/dist/taxonomy/write-service.d.ts +4 -2
- package/dist/taxonomy/write-service.js +8 -3
- package/dist/trash/write-service.js +19 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 — 2026-10-04
|
|
4
|
+
|
|
5
|
+
- Media: optional `BlobStorePort.sizeOf` reports original blob bytes (filesystem store memoizes `stat`); `MediaRecord.createdBy` is stamped at creation and write-once.
|
|
6
|
+
- Navigation: entry targets carry optional `lastKnownHref` and `entryType` hints.
|
|
7
|
+
- Taxonomy: `unassignTerms` returns the removed ids. Trash: a no-op trash/restore does not notify again.
|
|
8
|
+
- Internal `@jini-ai/*` dependencies are caret ranges (`workspace:^`) instead of exact pins, so a host on a newer patch resolves a single copy without an override.
|
|
9
|
+
|
|
3
10
|
## Unreleased — Trash host adoption
|
|
4
11
|
|
|
5
12
|
- Preserve `adapter-unavailable` for absent domains before checking entity policy; selected purge
|
|
@@ -5,6 +5,7 @@ export interface LocalFsBlobStoreDeps {
|
|
|
5
5
|
}
|
|
6
6
|
export declare class LocalFsBlobStore implements BlobStorePort {
|
|
7
7
|
private readonly rootDir;
|
|
8
|
+
private readonly sizesByKey;
|
|
8
9
|
constructor(deps: LocalFsBlobStoreDeps);
|
|
9
10
|
private resolvePath;
|
|
10
11
|
put(input: PutBlobInput): Promise<{
|
|
@@ -28,6 +29,9 @@ export declare class LocalFsBlobStore implements BlobStorePort {
|
|
|
28
29
|
get(input: {
|
|
29
30
|
storageKey: string;
|
|
30
31
|
}): Promise<Uint8Array>;
|
|
32
|
+
sizeOf({ storageKey }: {
|
|
33
|
+
storageKey: string;
|
|
34
|
+
}, _optional?: Record<string, never>): Promise<number | null>;
|
|
31
35
|
/**
|
|
32
36
|
* `ENOENT` (the path genuinely does not exist) resolves to `false`. Any other `stat()` failure
|
|
33
37
|
* (`EACCES` permission denied, `ENOTDIR`, `EIO`, ...) is rethrown raw — same "catch the one
|
|
@@ -16,6 +16,9 @@ import { dirname, join } from "node:path";
|
|
|
16
16
|
import { computeBlobStorageKey } from "./blob-key.js";
|
|
17
17
|
export class LocalFsBlobStore {
|
|
18
18
|
rootDir;
|
|
19
|
+
// Storage keys include workspace + content hash: immutable bytes make successful sizes
|
|
20
|
+
// reusable for this process. Promises coalesce concurrent reads of the same hash as well.
|
|
21
|
+
sizesByKey = new Map();
|
|
19
22
|
constructor(deps) {
|
|
20
23
|
this.rootDir = deps.rootDir;
|
|
21
24
|
}
|
|
@@ -27,6 +30,7 @@ export class LocalFsBlobStore {
|
|
|
27
30
|
const path = this.resolvePath(storageKey);
|
|
28
31
|
await mkdir(dirname(path), { recursive: true });
|
|
29
32
|
await writeFile(path, input.bytes);
|
|
33
|
+
this.sizesByKey.delete(storageKey);
|
|
30
34
|
return { storageKey };
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
@@ -46,6 +50,7 @@ export class LocalFsBlobStore {
|
|
|
46
50
|
await mkdir(dirname(path), { recursive: true });
|
|
47
51
|
try {
|
|
48
52
|
await writeFile(path, input.bytes, { flag: "wx" });
|
|
53
|
+
this.sizesByKey.delete(storageKey);
|
|
49
54
|
return { storageKey, written: true };
|
|
50
55
|
}
|
|
51
56
|
catch (err) {
|
|
@@ -58,6 +63,27 @@ export class LocalFsBlobStore {
|
|
|
58
63
|
async get(input) {
|
|
59
64
|
return readFile(this.resolvePath(input.storageKey));
|
|
60
65
|
}
|
|
66
|
+
sizeOf({ storageKey }, _optional = {}) {
|
|
67
|
+
const cached = this.sizesByKey.get(storageKey);
|
|
68
|
+
if (cached)
|
|
69
|
+
return cached;
|
|
70
|
+
const pending = stat(this.resolvePath(storageKey)).then((info) => info.size).catch((err) => {
|
|
71
|
+
if (err.code === "ENOENT")
|
|
72
|
+
return null;
|
|
73
|
+
throw err;
|
|
74
|
+
});
|
|
75
|
+
this.sizesByKey.set(storageKey, pending);
|
|
76
|
+
// Missing/restored blobs and transient failures must be retried. Identity guards prevent
|
|
77
|
+
// an older stat from clearing a newer entry after a write/remove invalidated the cache.
|
|
78
|
+
void pending.then((size) => {
|
|
79
|
+
if (size === null && this.sizesByKey.get(storageKey) === pending)
|
|
80
|
+
this.sizesByKey.delete(storageKey);
|
|
81
|
+
}, () => {
|
|
82
|
+
if (this.sizesByKey.get(storageKey) === pending)
|
|
83
|
+
this.sizesByKey.delete(storageKey);
|
|
84
|
+
});
|
|
85
|
+
return pending;
|
|
86
|
+
}
|
|
61
87
|
/**
|
|
62
88
|
* `ENOENT` (the path genuinely does not exist) resolves to `false`. Any other `stat()` failure
|
|
63
89
|
* (`EACCES` permission denied, `ENOTDIR`, `EIO`, ...) is rethrown raw — same "catch the one
|
|
@@ -79,6 +105,7 @@ export class LocalFsBlobStore {
|
|
|
79
105
|
}
|
|
80
106
|
async remove(input) {
|
|
81
107
|
await rm(this.resolvePath(input.storageKey), { force: true });
|
|
108
|
+
this.sizesByKey.delete(input.storageKey);
|
|
82
109
|
}
|
|
83
110
|
}
|
|
84
111
|
//# sourceMappingURL=blob-store.fs.js.map
|
|
@@ -24,6 +24,9 @@ export declare class InMemoryBlobStore implements BlobStorePort {
|
|
|
24
24
|
exists(input: {
|
|
25
25
|
storageKey: string;
|
|
26
26
|
}): Promise<boolean>;
|
|
27
|
+
sizeOf({ storageKey }: {
|
|
28
|
+
storageKey: string;
|
|
29
|
+
}, _optional?: Record<string, never>): Promise<number | null>;
|
|
27
30
|
remove(input: {
|
|
28
31
|
storageKey: string;
|
|
29
32
|
}): Promise<void>;
|
|
@@ -29,6 +29,9 @@ export class InMemoryBlobStore {
|
|
|
29
29
|
async exists(input) {
|
|
30
30
|
return this.bytesByKey.has(input.storageKey);
|
|
31
31
|
}
|
|
32
|
+
async sizeOf({ storageKey }, _optional = {}) {
|
|
33
|
+
return this.bytesByKey.get(storageKey)?.byteLength ?? null;
|
|
34
|
+
}
|
|
32
35
|
async remove(input) {
|
|
33
36
|
this.bytesByKey.delete(input.storageKey);
|
|
34
37
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MediaRecord } from "./types.js";
|
|
2
|
+
import type { MediaRepoPort } from "./ports.js";
|
|
3
|
+
/** Bind creation writes to one authenticated caller without shared mutable actor state.
|
|
4
|
+
* Useful when adapting an older upload service that predates asset-level attribution. */
|
|
5
|
+
export declare function mediaRepoWithCreator(required: {
|
|
6
|
+
mediaRepo: MediaRepoPort;
|
|
7
|
+
principalId: string;
|
|
8
|
+
}, _optional?: Record<string, never>): MediaRepoPort;
|
|
9
|
+
/** An update retains creation attribution, even when the original creator is unknown.
|
|
10
|
+
* Omitting an unknown field preserves compatibility with legacy serialized records. */
|
|
11
|
+
export declare function preserveMediaCreator(required: {
|
|
12
|
+
record: MediaRecord;
|
|
13
|
+
original: MediaRecord;
|
|
14
|
+
}, _optional?: Record<string, never>): MediaRecord;
|
|
15
|
+
//# sourceMappingURL=created-by.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Bind creation writes to one authenticated caller without shared mutable actor state.
|
|
2
|
+
* Useful when adapting an older upload service that predates asset-level attribution. */
|
|
3
|
+
export function mediaRepoWithCreator(required, _optional = {}) {
|
|
4
|
+
const { mediaRepo, principalId } = required;
|
|
5
|
+
return {
|
|
6
|
+
findById: input => mediaRepo.findById(input),
|
|
7
|
+
findBySlug: input => mediaRepo.findBySlug(input),
|
|
8
|
+
list: input => mediaRepo.list(input),
|
|
9
|
+
remove: input => mediaRepo.remove(input),
|
|
10
|
+
save: record => mediaRepo.save({ ...record, createdBy: principalId }),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** An update retains creation attribution, even when the original creator is unknown.
|
|
14
|
+
* Omitting an unknown field preserves compatibility with legacy serialized records. */
|
|
15
|
+
export function preserveMediaCreator(required, _optional = {}) {
|
|
16
|
+
const { createdBy: _ignored, ...metadata } = required.record;
|
|
17
|
+
return { ...metadata, ...(required.original.createdBy !== undefined
|
|
18
|
+
? { createdBy: required.original.createdBy } : {}) };
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=created-by.js.map
|
package/dist/media/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type { MediaStatus, MediaSource, MediaRecord, AssetBlobStatus, AssetBlobR
|
|
|
15
15
|
export { MediaNotFoundError, MediaValidationError, MediaConflictError, MediaSourceImmutableError, MediaStillReferencedError, } from "./types.js";
|
|
16
16
|
export type { MediaRepoPort, AssetBlobRepoPort, AssetRenditionRepoPort, BlobStorePort, BlobGcJournalRepoPort, PutBlobInput, TransformDefinitionRepoPort, } from "./ports.js";
|
|
17
17
|
export { computeBlobStorageKey } from "./blob-key.js";
|
|
18
|
+
export { preserveMediaCreator, mediaRepoWithCreator } from "./created-by.js";
|
|
18
19
|
export { MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES, isAllowedMediaHtmlAttributeName, parseMediaHtmlAttributes, describeMediaHtmlAttributeError, type MediaHtmlAttributeRejectionReason, type MediaHtmlAttributeError, type ParsedMediaHtmlAttributes, } from "./html-attributes.js";
|
|
19
20
|
export { InMemoryMediaRepo, InMemoryAssetBlobRepo, InMemoryAssetRenditionRepo, InMemoryBlobGcJournalRepo, InMemoryTransformDefinitionRepo, } from "./repo.memory.js";
|
|
20
21
|
export { withSha256Lock } from "./blob-gc-lock.js";
|
package/dist/media/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { MediaNotFoundError, MediaValidationError, MediaConflictError, MediaSourceImmutableError, MediaStillReferencedError, } from "./types.js";
|
|
2
2
|
export { computeBlobStorageKey } from "./blob-key.js";
|
|
3
|
+
export { preserveMediaCreator, mediaRepoWithCreator } from "./created-by.js";
|
|
3
4
|
export { MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES, isAllowedMediaHtmlAttributeName, parseMediaHtmlAttributes, describeMediaHtmlAttributeError, } from "./html-attributes.js";
|
|
4
5
|
export { InMemoryMediaRepo, InMemoryAssetBlobRepo, InMemoryAssetRenditionRepo, InMemoryBlobGcJournalRepo, InMemoryTransformDefinitionRepo, } from "./repo.memory.js";
|
|
5
6
|
export { withSha256Lock } from "./blob-gc-lock.js";
|
|
@@ -60,7 +60,7 @@ export interface UploadMediaInput {
|
|
|
60
60
|
alt?: string | undefined;
|
|
61
61
|
caption?: string | undefined;
|
|
62
62
|
credit?: string | undefined;
|
|
63
|
-
/**
|
|
63
|
+
/** Acting principal, stamped on each media row and on a newly created asset_blobs row. */
|
|
64
64
|
createdByPrincipal: string;
|
|
65
65
|
}
|
|
66
66
|
export interface UploadMediaDeps {
|
package/dist/media/ports.d.ts
CHANGED
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
*
|
|
14
14
|
* `BlobStorePort`'s surface is deliberately narrower than the full
|
|
15
15
|
* design: no `capabilities()`/`createPresignedUpload()` (the presign surface is
|
|
16
|
-
* explicitly deferred by this task's scope). It
|
|
17
|
-
*
|
|
16
|
+
* explicitly deferred by this task's scope). It extends the original minimal
|
|
17
|
+
* 3 (`put`/`get`/`exists`) with `remove()`. That addition is
|
|
18
18
|
* necessary to implement `purgeMedia`'s explicitly-requested "best-effort blob
|
|
19
19
|
* unlink" (see `media-service.ts`) — without a way to delete bytes, purge could
|
|
20
20
|
* only ever delete rows, never reclaim storage. This is disclosed here, not
|
|
21
21
|
* silently smuggled in: it is still far short of the real journaled
|
|
22
22
|
* GC protocol (no epochs, no `BEGIN IMMEDIATE`, no crash-safety guarantee).
|
|
23
|
+
* `putIfAbsent` adds atomic create-only writes; optional `sizeOf` reports original bytes
|
|
24
|
+
* without reading payloads (successful filesystem sizes are cached by content-addressed key).
|
|
23
25
|
*
|
|
24
26
|
* Interfaces only — no feature logic.
|
|
25
27
|
*/
|
|
@@ -162,6 +164,14 @@ export interface BlobStorePort {
|
|
|
162
164
|
get(input: {
|
|
163
165
|
storageKey: string;
|
|
164
166
|
}): Promise<Uint8Array>;
|
|
167
|
+
/**
|
|
168
|
+
* Original byte count without reading the payload; null only when the key is absent.
|
|
169
|
+
* Other storage failures reject. Optional for older/custom adapters; consumers must report
|
|
170
|
+
* unknown size when unavailable, never download an entire blob just to count bytes.
|
|
171
|
+
*/
|
|
172
|
+
sizeOf?(required: {
|
|
173
|
+
storageKey: string;
|
|
174
|
+
}, optional?: Record<string, never>): Promise<number | null>;
|
|
165
175
|
exists(input: {
|
|
166
176
|
storageKey: string;
|
|
167
177
|
}): Promise<boolean>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { preserveMediaCreator } from "./created-by.js";
|
|
1
2
|
export class InMemoryMediaRepo {
|
|
2
3
|
rows;
|
|
3
4
|
constructor(requiredArgs, optionalArgs = {}) {
|
|
@@ -19,7 +20,7 @@ export class InMemoryMediaRepo {
|
|
|
19
20
|
this.rows.push(record);
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
|
-
this.rows[index] = record;
|
|
23
|
+
this.rows[index] = preserveMediaCreator({ record, original: this.rows[index] });
|
|
23
24
|
}
|
|
24
25
|
async remove(required) {
|
|
25
26
|
this.rows = this.rows.filter((row) => !(row.workspaceId === required.workspaceId && row.id === required.id));
|
package/dist/media/types.d.ts
CHANGED
|
@@ -41,6 +41,10 @@ export interface MediaSource {
|
|
|
41
41
|
export interface MediaRecord {
|
|
42
42
|
id: UUID;
|
|
43
43
|
workspaceId: UUID;
|
|
44
|
+
/** Acting principal at asset creation, independent of the producer/model and editable credit.
|
|
45
|
+
* Optional/null for records written before attribution existed. Never inferred from a shared
|
|
46
|
+
* blob's owner: identical bytes may have been uploaded by different principals. */
|
|
47
|
+
createdBy?: string | null | undefined;
|
|
44
48
|
title: string;
|
|
45
49
|
/**
|
|
46
50
|
* Human-memorable, unique-per-workspace lookup key (owner-directed, 2026-09-07) — an ADDITIONAL
|
|
@@ -241,6 +241,12 @@ function validateTarget(target) {
|
|
|
241
241
|
if (!VALID_TARGET_KINDS.has(kind)) {
|
|
242
242
|
throw new MenuValidationError({ message: `unknown target kind '${kind}'` });
|
|
243
243
|
}
|
|
244
|
+
if (kind === "entryRef" && target.lastKnownHref !== undefined) {
|
|
245
|
+
const rawHref = target.lastKnownHref;
|
|
246
|
+
if (typeof rawHref !== "string" || !rawHref.trim() || !isAllowedHref({ rawHref })) {
|
|
247
|
+
throw new MenuValidationError({ message: `entryRef lastKnownHref must be ${ALLOWED_HREF_SHAPES_DESCRIPTION}.` });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
244
250
|
if (kind === "url") {
|
|
245
251
|
const rawHref = target.href;
|
|
246
252
|
if (typeof rawHref !== "string" || !rawHref.trim()) {
|
|
@@ -49,6 +49,11 @@ export type ReservedNavTargetKind = "dynamicQuery" | "content";
|
|
|
49
49
|
/** A link to another content entry (page/post/media/…). Integrity-tracked. */
|
|
50
50
|
export interface NavEntryTarget {
|
|
51
51
|
readonly kind: "entryRef";
|
|
52
|
+
/** Optional content-type hint for editors; reference integrity and routing still use entryId. */
|
|
53
|
+
readonly entryType?: string;
|
|
54
|
+
/** Author's last known URL, retained for exports and routing misses. A known unavailable
|
|
55
|
+
* entry must remain hidden; this snapshot never overrides an availability decision. */
|
|
56
|
+
readonly lastKnownHref?: string;
|
|
52
57
|
/** Target entry id. Extracted to `entry_refs` at the write chokepoint. */
|
|
53
58
|
readonly entryId: UUID;
|
|
54
59
|
}
|
|
@@ -352,12 +352,14 @@ export interface UnassignTermsRequired {
|
|
|
352
352
|
* authorize -> validateAssignmentTarget -> write -> stamp -> outbox chokepoint as `assignTerms`,
|
|
353
353
|
* and — like `assignTerms` — never produces a `taxonomy_revisions` row ( disclosed
|
|
354
354
|
* narrowing applies equally to removal). Idempotent: unassigning a row that isn't there is a no-op,
|
|
355
|
-
* not an error
|
|
355
|
+
* not an error. Only removed ids are returned, stamped, and emitted; an entirely absent set is a no-op. Chosen over
|
|
356
356
|
* a "set terms" replace call — see the taxonomy plan's "What the docs settle" #3 for why replace
|
|
357
357
|
* risks silently erasing a concurrent assignment.
|
|
358
358
|
* See docs/decisions/DR-005-ordered-taxonomy-validation.md.
|
|
359
359
|
*/
|
|
360
|
-
export declare function unassignTerms(required: UnassignTermsRequired, _optional?: Record<string, never>): Promise<
|
|
360
|
+
export declare function unassignTerms(required: UnassignTermsRequired, _optional?: Record<string, never>): Promise<{
|
|
361
|
+
removedTermIds: string[];
|
|
362
|
+
}>;
|
|
361
363
|
export interface EntryTermsCleanupPort {
|
|
362
364
|
deleteByContent(params: {
|
|
363
365
|
workspaceId: string;
|
|
@@ -408,7 +408,7 @@ export async function assignTerms(required, _optional = {}) {
|
|
|
408
408
|
* authorize -> validateAssignmentTarget -> write -> stamp -> outbox chokepoint as `assignTerms`,
|
|
409
409
|
* and — like `assignTerms` — never produces a `taxonomy_revisions` row ( disclosed
|
|
410
410
|
* narrowing applies equally to removal). Idempotent: unassigning a row that isn't there is a no-op,
|
|
411
|
-
* not an error
|
|
411
|
+
* not an error. Only removed ids are returned, stamped, and emitted; an entirely absent set is a no-op. Chosen over
|
|
412
412
|
* a "set terms" replace call — see the taxonomy plan's "What the docs settle" #3 for why replace
|
|
413
413
|
* risks silently erasing a concurrent assignment.
|
|
414
414
|
* See docs/decisions/DR-005-ordered-taxonomy-validation.md.
|
|
@@ -417,19 +417,24 @@ export async function unassignTerms(required, _optional = {}) {
|
|
|
417
417
|
const { deps, principalId, contentType, contentId, termIds } = required;
|
|
418
418
|
await authorizeTaxonomyManage(deps, principalId);
|
|
419
419
|
await validateAssignmentTarget(deps, contentType, contentId, termIds);
|
|
420
|
+
const removedTermIds = [];
|
|
420
421
|
for (const termId of termIds) {
|
|
421
|
-
await deps.entryTerms.remove({ contentType, contentId, termId })
|
|
422
|
+
if (await deps.entryTerms.remove({ contentType, contentId, termId }) > 0)
|
|
423
|
+
removedTermIds.push(termId);
|
|
422
424
|
}
|
|
425
|
+
if (removedTermIds.length === 0)
|
|
426
|
+
return { removedTermIds };
|
|
423
427
|
const now = kernelNowIso({ clock: deps.clock });
|
|
424
428
|
await deps.stampWatermark({});
|
|
425
429
|
await deps.outbox.enqueue({ event: {
|
|
426
430
|
name: "taxonomy.terms_unassigned",
|
|
427
431
|
contentType,
|
|
428
432
|
contentId,
|
|
429
|
-
termIds,
|
|
433
|
+
termIds: removedTermIds,
|
|
430
434
|
actorId: principalId,
|
|
431
435
|
occurredAt: now,
|
|
432
436
|
} });
|
|
437
|
+
return { removedTermIds };
|
|
433
438
|
}
|
|
434
439
|
/** Content-deletion event subscriber. Best-effort (a missed event leaves an orphaned
|
|
435
440
|
* `entry_terms` row that is inert on read; a periodic/boot reconciliation sweep — modeled by
|
|
@@ -95,12 +95,16 @@ export function createTrashService(deps, optional = {}) {
|
|
|
95
95
|
priorMarker: marker.priorMarker ?? trashOptional.priorMarker ?? null,
|
|
96
96
|
};
|
|
97
97
|
await deps.repo.insert({ row });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
// A successful noop may repair a missing index, but did not change the domain marker.
|
|
99
|
+
// Repeated removal must not emit another recovery watermark or outbox event (F1487).
|
|
100
|
+
if (!marker.noop) {
|
|
101
|
+
await notifyChanged(optional, {
|
|
102
|
+
workspaceId: required.workspaceId,
|
|
103
|
+
entityType: required.entityType,
|
|
104
|
+
entityId: required.entityId,
|
|
105
|
+
change: "trash",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
104
108
|
return marker;
|
|
105
109
|
}
|
|
106
110
|
});
|
|
@@ -135,12 +139,15 @@ export function createTrashService(deps, optional = {}) {
|
|
|
135
139
|
if (!marker.ok)
|
|
136
140
|
return marker.reason === "not-found" ? "not-found" : "version-changed";
|
|
137
141
|
await deps.repo.deleteById({ workspaceId: required.workspaceId, id: row.id });
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// An already-clear marker still needs index cleanup, without a duplicate notification.
|
|
143
|
+
if (!marker.noop) {
|
|
144
|
+
await notifyChanged(optional, {
|
|
145
|
+
workspaceId: required.workspaceId,
|
|
146
|
+
entityType: required.entityType,
|
|
147
|
+
entityId: required.entityId,
|
|
148
|
+
change: "restore",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
144
151
|
return "restored";
|
|
145
152
|
}
|
|
146
153
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/cms",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"@jini-ai/http-kit": "0.4.0"
|
|
133
133
|
},
|
|
134
134
|
"dependencies": {
|
|
135
|
-
"@jini-ai/core": "0.4.0"
|
|
135
|
+
"@jini-ai/core": "^0.4.0"
|
|
136
136
|
},
|
|
137
137
|
"peerDependencies": {
|
|
138
138
|
"sharp": "^0.35.3",
|