@agent-native/creative-context 0.3.0 → 0.5.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/README.md +51 -9
- package/agent-native.package.json +4 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-context-memberships.d.ts +11 -0
- package/dist/actions/list-context-memberships.d.ts.map +1 -0
- package/dist/actions/list-context-memberships.js +47 -0
- package/dist/actions/list-context-memberships.js.map +1 -0
- package/dist/actions/list-creative-contexts.d.ts +12 -0
- package/dist/actions/list-creative-contexts.d.ts.map +1 -0
- package/dist/actions/list-creative-contexts.js +28 -0
- package/dist/actions/list-creative-contexts.js.map +1 -0
- package/dist/actions/manage-context-membership.d.ts +64 -0
- package/dist/actions/manage-context-membership.d.ts.map +1 -0
- package/dist/actions/manage-context-membership.js +70 -0
- package/dist/actions/manage-context-membership.js.map +1 -0
- package/dist/actions/manage-context-pack.d.ts +3 -0
- package/dist/actions/manage-context-pack.js +6 -0
- package/dist/actions/manage-context-pack.js.map +1 -1
- package/dist/actions/manage-context-source.js +4 -11
- package/dist/actions/manage-context-source.js.map +1 -1
- package/dist/actions/manage-creative-context.d.ts +28 -0
- package/dist/actions/manage-creative-context.d.ts.map +1 -0
- package/dist/actions/manage-creative-context.js +65 -0
- package/dist/actions/manage-creative-context.js.map +1 -0
- package/dist/actions/process-context-purge.d.ts +16 -1
- package/dist/actions/process-context-purge.js +2 -1
- package/dist/actions/process-context-purge.js.map +1 -1
- package/dist/actions/search-creative-context.d.ts +1 -0
- package/dist/actions/search-creative-context.js +2 -1
- package/dist/actions/search-creative-context.js.map +1 -1
- package/dist/client/CreativeContextChip.d.ts +5 -1
- package/dist/client/CreativeContextChip.d.ts.map +1 -1
- package/dist/client/CreativeContextChip.js +36 -7
- package/dist/client/CreativeContextChip.js.map +1 -1
- package/dist/client/CreativeContextPanel.d.ts.map +1 -1
- package/dist/client/CreativeContextPanel.js +406 -89
- package/dist/client/CreativeContextPanel.js.map +1 -1
- package/dist/client/CreativeContextShareTab.d.ts +56 -0
- package/dist/client/CreativeContextShareTab.d.ts.map +1 -0
- package/dist/client/CreativeContextShareTab.js +213 -0
- package/dist/client/CreativeContextShareTab.js.map +1 -0
- package/dist/client/actions.d.ts +210 -0
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +318 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/application-state.d.ts +2 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +25 -2
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +77 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/connectors/types.d.ts +2 -2
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +1592 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +95 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +80 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/server/generation-artifact-access.js +2 -0
- package/dist/server/generation-artifact-access.js.map +1 -1
- package/dist/server/generation-context.d.ts +12 -4
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +129 -7
- package/dist/server/generation-context.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +16 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/isolated-a2a.d.ts +3 -0
- package/dist/server/isolated-a2a.d.ts.map +1 -1
- package/dist/server/isolated-a2a.js +3 -1
- package/dist/server/isolated-a2a.js.map +1 -1
- package/dist/server/media.d.ts +1 -1
- package/dist/server/media.d.ts.map +1 -1
- package/dist/server/media.js +24 -9
- package/dist/server/media.js.map +1 -1
- package/dist/server/native-resource-capture.d.ts +78 -0
- package/dist/server/native-resource-capture.d.ts.map +1 -0
- package/dist/server/native-resource-capture.js +94 -0
- package/dist/server/native-resource-capture.js.map +1 -0
- package/dist/server/public-serialization.js +1 -1
- package/dist/server/public-serialization.js.map +1 -1
- package/dist/server/retrieval.d.ts +1 -0
- package/dist/server/retrieval.d.ts.map +1 -1
- package/dist/server/retrieval.js +2 -0
- package/dist/server/retrieval.js.map +1 -1
- package/dist/server/safe-native-preview.d.ts +15 -0
- package/dist/server/safe-native-preview.d.ts.map +1 -0
- package/dist/server/safe-native-preview.js +149 -0
- package/dist/server/safe-native-preview.js.map +1 -0
- package/dist/store/brand.d.ts.map +1 -1
- package/dist/store/brand.js +2 -16
- package/dist/store/brand.js.map +1 -1
- package/dist/store/content.d.ts +1 -0
- package/dist/store/content.d.ts.map +1 -1
- package/dist/store/content.js +84 -20
- package/dist/store/content.js.map +1 -1
- package/dist/store/contexts.d.ts +216 -0
- package/dist/store/contexts.d.ts.map +1 -0
- package/dist/store/contexts.js +1341 -0
- package/dist/store/contexts.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/packs.d.ts +3 -0
- package/dist/store/packs.d.ts.map +1 -1
- package/dist/store/packs.js +126 -9
- package/dist/store/packs.js.map +1 -1
- package/dist/store/purge.d.ts.map +1 -1
- package/dist/store/purge.js +103 -6
- package/dist/store/purge.js.map +1 -1
- package/dist/store/sources.d.ts.map +1 -1
- package/dist/store/sources.js +2 -15
- package/dist/store/sources.js.map +1 -1
- package/dist/store/suggestions.d.ts.map +1 -1
- package/dist/store/suggestions.js +3 -1
- package/dist/store/suggestions.js.map +1 -1
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/docs/skills/creative-context/SKILL.md +13 -5
- package/package.json +3 -3
- package/src/actions/index.ts +8 -0
- package/src/actions/job-public-serialization.spec.ts +21 -0
- package/src/actions/list-context-memberships.ts +56 -0
- package/src/actions/list-creative-contexts.spec.ts +38 -0
- package/src/actions/list-creative-contexts.ts +32 -0
- package/src/actions/manage-context-membership.ts +80 -0
- package/src/actions/manage-context-pack.ts +6 -0
- package/src/actions/manage-context-source.ts +8 -11
- package/src/actions/manage-creative-context.ts +73 -0
- package/src/actions/process-context-purge.ts +4 -1
- package/src/actions/search-creative-context.ts +2 -1
- package/src/client/CreativeContextChip.spec.ts +32 -0
- package/src/client/CreativeContextChip.tsx +83 -14
- package/src/client/CreativeContextPanel.tsx +1947 -885
- package/src/client/CreativeContextShareTab.spec.ts +70 -0
- package/src/client/CreativeContextShareTab.tsx +657 -0
- package/src/client/actions.spec.ts +177 -0
- package/src/client/actions.ts +566 -0
- package/src/client/application-state.spec.ts +5 -0
- package/src/client/application-state.ts +30 -2
- package/src/client/index.ts +33 -0
- package/src/client/messages.ts +95 -0
- package/src/connectors/types.ts +6 -8
- package/src/eval/library-flow.acceptance.spec.tsx +1 -0
- package/src/schema/index.ts +116 -0
- package/src/schema/migrations.ts +80 -0
- package/src/server/generation-artifact-access.ts +1 -0
- package/src/server/generation-context-a2a.spec.ts +133 -0
- package/src/server/generation-context.ts +153 -7
- package/src/server/index.spec.ts +15 -0
- package/src/server/index.ts +16 -0
- package/src/server/isolated-a2a.ts +4 -1
- package/src/server/media.spec.ts +30 -2
- package/src/server/media.ts +31 -12
- package/src/server/native-resource-capture.spec.ts +121 -0
- package/src/server/native-resource-capture.ts +198 -0
- package/src/server/public-serialization.spec.ts +1 -0
- package/src/server/public-serialization.ts +1 -1
- package/src/server/retrieval.ts +3 -0
- package/src/server/safe-native-preview.spec.ts +29 -0
- package/src/server/safe-native-preview.ts +200 -0
- package/src/store/access.integration.spec.ts +1116 -0
- package/src/store/brand.ts +2 -21
- package/src/store/content.ts +217 -17
- package/src/store/contexts.ts +1826 -0
- package/src/store/index.ts +1 -0
- package/src/store/packs.ts +279 -24
- package/src/store/purge.ts +139 -6
- package/src/store/sources.ts +2 -18
- package/src/store/suggestions.ts +3 -1
- package/src/types.ts +95 -0
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
import { assertCurrentRequestUserIsOrgAdmin, currentRequestUserIsOrgAdmin, } from "@agent-native/core/server";
|
|
2
|
+
import { accessFilter, assertAccess, resolveAccess, } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, asc, count, eq, gt, inArray, isNotNull, isNull, or, } from "drizzle-orm";
|
|
4
|
+
import { creativeContextMediaUrl } from "../media-url.js";
|
|
5
|
+
import { getCreativeContext } from "../server/context.js";
|
|
6
|
+
import { captureNativeCreativeResource, parseNativeCreativeArtifactKey, resolveNativeCreativeResourceUpdateStatuses, } from "../server/native-resource-capture.js";
|
|
7
|
+
import { sanitizePublicMetadata } from "../server/public-serialization.js";
|
|
8
|
+
import { getCreativeContextItem } from "./content.js";
|
|
9
|
+
import { newId, nowIso, parseJson, requireActor, stringifyJson, } from "./helpers.js";
|
|
10
|
+
function defaultContextScopeKey(actor) {
|
|
11
|
+
return actor.orgId ? `org:${actor.orgId}` : `user:${actor.ownerEmail}`;
|
|
12
|
+
}
|
|
13
|
+
function mapContext(row, memberCount = 0, role = "viewer") {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
name: row.name,
|
|
17
|
+
description: row.description ?? null,
|
|
18
|
+
kind: row.kind,
|
|
19
|
+
brandProfileId: row.brandProfileId ?? null,
|
|
20
|
+
approvalPolicy: row.approvalPolicy,
|
|
21
|
+
archivedAt: row.archivedAt ?? null,
|
|
22
|
+
visibility: row.visibility,
|
|
23
|
+
memberCount,
|
|
24
|
+
createdAt: row.createdAt,
|
|
25
|
+
updatedAt: row.updatedAt,
|
|
26
|
+
access: {
|
|
27
|
+
role,
|
|
28
|
+
canSubmit: role !== "viewer",
|
|
29
|
+
canReview: role === "owner" || role === "admin",
|
|
30
|
+
canAdmin: role === "owner" || role === "admin",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function mapMembership(row) {
|
|
35
|
+
return {
|
|
36
|
+
id: row.id,
|
|
37
|
+
contextId: row.contextId,
|
|
38
|
+
artifactKey: row.artifactKey,
|
|
39
|
+
publishedItemId: row.publishedItemId ?? null,
|
|
40
|
+
publishedItemVersionId: row.publishedItemVersionId ?? null,
|
|
41
|
+
pendingSubmissionId: row.pendingSubmissionId ?? null,
|
|
42
|
+
rank: row.rank,
|
|
43
|
+
purpose: row.purpose ?? null,
|
|
44
|
+
status: row.status,
|
|
45
|
+
createdAt: row.createdAt,
|
|
46
|
+
updatedAt: row.updatedAt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function mapSubmission(row) {
|
|
50
|
+
return {
|
|
51
|
+
id: row.id,
|
|
52
|
+
contextId: row.contextId,
|
|
53
|
+
membershipId: row.membershipId,
|
|
54
|
+
artifactKey: row.artifactKey,
|
|
55
|
+
publishedItemId: row.publishedItemId ?? null,
|
|
56
|
+
publishedItemVersionId: row.publishedItemVersionId ?? null,
|
|
57
|
+
note: row.note ?? null,
|
|
58
|
+
status: row.status,
|
|
59
|
+
submittedBy: row.submittedBy,
|
|
60
|
+
reviewedBy: row.reviewedBy ?? null,
|
|
61
|
+
reviewNote: row.reviewNote ?? null,
|
|
62
|
+
createdAt: row.createdAt,
|
|
63
|
+
reviewedAt: row.reviewedAt ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function appendAudit(tx, contextId, operation, details) {
|
|
67
|
+
const { schema } = getCreativeContext();
|
|
68
|
+
const actor = requireActor();
|
|
69
|
+
await tx.insert(schema.creativeContextAudit).values({
|
|
70
|
+
id: newId("cca"),
|
|
71
|
+
contextId,
|
|
72
|
+
operation,
|
|
73
|
+
actorEmail: actor.ownerEmail,
|
|
74
|
+
details: stringifyJson(details),
|
|
75
|
+
createdAt: nowIso(),
|
|
76
|
+
ownerEmail: actor.ownerEmail,
|
|
77
|
+
orgId: actor.orgId,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function assertContextRole(contextId, role) {
|
|
81
|
+
return assertAccess("creative-context", contextId, role, undefined, {
|
|
82
|
+
skipResourceBody: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function requireReviewer(contextId) {
|
|
86
|
+
await assertContextRole(contextId, "admin");
|
|
87
|
+
}
|
|
88
|
+
function normalizedFromDetail(detail) {
|
|
89
|
+
return {
|
|
90
|
+
externalId: detail.item.externalId,
|
|
91
|
+
kind: detail.item.kind,
|
|
92
|
+
title: detail.version.title,
|
|
93
|
+
canonicalUrl: detail.item.canonicalUrl ?? undefined,
|
|
94
|
+
mimeType: detail.version.mimeType ?? undefined,
|
|
95
|
+
content: detail.version.content,
|
|
96
|
+
summary: detail.version.summary ?? undefined,
|
|
97
|
+
contentHash: detail.version.contentHash,
|
|
98
|
+
sourceModifiedAt: detail.version.sourceModifiedAt ?? undefined,
|
|
99
|
+
sourceVersion: detail.version.sourceVersion ?? undefined,
|
|
100
|
+
rawSnapshotBlobRef: detail.version.rawSnapshotBlobRef ?? undefined,
|
|
101
|
+
parseStatus: detail.version.parseStatus,
|
|
102
|
+
parseError: detail.version.parseError ?? undefined,
|
|
103
|
+
upstreamAccess: detail.item.upstreamAccess,
|
|
104
|
+
curationStatus: "included",
|
|
105
|
+
curationRank: detail.item.curationRank === "ignored"
|
|
106
|
+
? "normal"
|
|
107
|
+
: detail.item.curationRank,
|
|
108
|
+
tags: detail.item.tags,
|
|
109
|
+
colors: detail.item.colors,
|
|
110
|
+
provenance: detail.item.provenance,
|
|
111
|
+
thumbnailBlobRef: detail.item.thumbnailBlobRef ?? undefined,
|
|
112
|
+
// Keep compiler/reassembly manifests while stripping capability-like metadata.
|
|
113
|
+
metadata: (sanitizePublicMetadata(detail.version.metadata) ?? {}),
|
|
114
|
+
chunks: detail.chunks.map((chunk) => ({
|
|
115
|
+
ordinal: chunk.ordinal,
|
|
116
|
+
kind: chunk.kind,
|
|
117
|
+
text: chunk.text,
|
|
118
|
+
...(chunk.startOffset === null ? {} : { startOffset: chunk.startOffset }),
|
|
119
|
+
...(chunk.endOffset === null ? {} : { endOffset: chunk.endOffset }),
|
|
120
|
+
...(chunk.tokenCount === null ? {} : { tokenCount: chunk.tokenCount }),
|
|
121
|
+
metadata: chunk.metadata,
|
|
122
|
+
})),
|
|
123
|
+
media: detail.media.map((media) => ({
|
|
124
|
+
kind: media.kind,
|
|
125
|
+
...(media.mimeType ? { mimeType: media.mimeType } : {}),
|
|
126
|
+
accessMode: media.accessMode,
|
|
127
|
+
...(media.url ? { url: media.url } : {}),
|
|
128
|
+
...(media.storageKey ? { storageKey: media.storageKey } : {}),
|
|
129
|
+
...(media.provenanceUrl ? { provenanceUrl: media.provenanceUrl } : {}),
|
|
130
|
+
...(media.altText ? { altText: media.altText } : {}),
|
|
131
|
+
...(media.caption ? { caption: media.caption } : {}),
|
|
132
|
+
captionStatus: media.captionStatus,
|
|
133
|
+
...(media.ocrText ? { ocrText: media.ocrText } : {}),
|
|
134
|
+
palette: media.palette,
|
|
135
|
+
...(media.contentHash ? { contentHash: media.contentHash } : {}),
|
|
136
|
+
...(media.width === null ? {} : { width: media.width }),
|
|
137
|
+
...(media.height === null ? {} : { height: media.height }),
|
|
138
|
+
...(media.durationMs === null ? {} : { durationMs: media.durationMs }),
|
|
139
|
+
metadata: media.metadata,
|
|
140
|
+
})),
|
|
141
|
+
edges: detail.edges.map((edge) => ({
|
|
142
|
+
relation: edge.relation,
|
|
143
|
+
...(edge.toItemId ? { toItemId: edge.toItemId } : {}),
|
|
144
|
+
...(edge.toItemVersionId
|
|
145
|
+
? { toItemVersionId: edge.toItemVersionId }
|
|
146
|
+
: {}),
|
|
147
|
+
...(edge.toExternalId ? { toExternalId: edge.toExternalId } : {}),
|
|
148
|
+
metadata: edge.metadata,
|
|
149
|
+
})),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function writeSnapshot(input) {
|
|
153
|
+
const { getDb, schema } = getCreativeContext();
|
|
154
|
+
const timestamp = nowIso();
|
|
155
|
+
const existing = await getDb()
|
|
156
|
+
.select()
|
|
157
|
+
.from(schema.contextItems)
|
|
158
|
+
.where(and(eq(schema.contextItems.sourceId, input.sourceId), eq(schema.contextItems.externalId, input.artifactKey)))
|
|
159
|
+
.limit(1);
|
|
160
|
+
const previous = existing[0];
|
|
161
|
+
const itemId = previous?.id ?? newId("cci");
|
|
162
|
+
const itemVersionId = newId("ccv");
|
|
163
|
+
const versionNumber = previous
|
|
164
|
+
? (await getDb()
|
|
165
|
+
.select({ versionNumber: schema.contextItemVersions.versionNumber })
|
|
166
|
+
.from(schema.contextItemVersions)
|
|
167
|
+
.where(eq(schema.contextItemVersions.id, previous.currentVersionId))
|
|
168
|
+
.limit(1))[0]?.versionNumber + 1
|
|
169
|
+
: 1;
|
|
170
|
+
const itemValues = {
|
|
171
|
+
kind: input.item.kind,
|
|
172
|
+
title: input.item.title,
|
|
173
|
+
canonicalUrl: input.item.canonicalUrl ?? null,
|
|
174
|
+
mimeType: input.item.mimeType ?? null,
|
|
175
|
+
currentVersionId: itemVersionId,
|
|
176
|
+
currentContentHash: input.item.contentHash,
|
|
177
|
+
status: "active",
|
|
178
|
+
upstreamAccess: input.item.upstreamAccess ?? "available",
|
|
179
|
+
curationStatus: "included",
|
|
180
|
+
curationRank: input.item.curationRank ?? "normal",
|
|
181
|
+
starred: input.item.starred ? 1 : 0,
|
|
182
|
+
inventoryState: input.item.inventoryState ?? "available",
|
|
183
|
+
indexState: input.item.indexState ?? "pending",
|
|
184
|
+
tags: stringifyJson(input.item.tags ?? []),
|
|
185
|
+
colors: stringifyJson(input.item.colors ?? (input.item.color ? [input.item.color] : [])),
|
|
186
|
+
sortOrder: input.item.sortOrder ?? 0,
|
|
187
|
+
parentItemId: input.item.parentItemId ?? null,
|
|
188
|
+
provenance: stringifyJson(input.item.provenance),
|
|
189
|
+
thumbnailBlobRef: input.item.thumbnailBlobRef ?? null,
|
|
190
|
+
metadata: stringifyJson(input.item.metadata),
|
|
191
|
+
updatedAt: timestamp,
|
|
192
|
+
};
|
|
193
|
+
await getDb().transaction(async (tx) => {
|
|
194
|
+
if (previous)
|
|
195
|
+
await tx
|
|
196
|
+
.update(schema.contextItems)
|
|
197
|
+
.set(itemValues)
|
|
198
|
+
.where(eq(schema.contextItems.id, itemId));
|
|
199
|
+
else
|
|
200
|
+
await tx.insert(schema.contextItems).values({
|
|
201
|
+
id: itemId,
|
|
202
|
+
sourceId: input.sourceId,
|
|
203
|
+
externalId: input.artifactKey,
|
|
204
|
+
...itemValues,
|
|
205
|
+
createdAt: timestamp,
|
|
206
|
+
ownerEmail: input.ownerEmail,
|
|
207
|
+
orgId: input.orgId,
|
|
208
|
+
});
|
|
209
|
+
await tx.insert(schema.contextItemVersions).values({
|
|
210
|
+
id: itemVersionId,
|
|
211
|
+
itemId,
|
|
212
|
+
versionNumber: Number(versionNumber ?? 1),
|
|
213
|
+
contentHash: input.item.contentHash,
|
|
214
|
+
title: input.item.title,
|
|
215
|
+
content: input.item.content,
|
|
216
|
+
summary: input.item.summary ?? null,
|
|
217
|
+
mimeType: input.item.mimeType ?? null,
|
|
218
|
+
sourceModifiedAt: input.item.sourceModifiedAt ?? null,
|
|
219
|
+
sourceVersion: input.item.sourceVersion ?? null,
|
|
220
|
+
rawSnapshotBlobRef: input.item.rawSnapshotBlobRef ?? null,
|
|
221
|
+
parseStatus: input.item.parseStatus ?? "parsed",
|
|
222
|
+
parseError: input.item.parseError ?? null,
|
|
223
|
+
metadata: stringifyJson(input.item.metadata),
|
|
224
|
+
createdAt: timestamp,
|
|
225
|
+
ownerEmail: input.ownerEmail,
|
|
226
|
+
orgId: input.orgId,
|
|
227
|
+
});
|
|
228
|
+
const chunks = input.item.chunks?.length
|
|
229
|
+
? input.item.chunks
|
|
230
|
+
: [{ ordinal: 0, kind: "text", text: input.item.content }];
|
|
231
|
+
await tx.insert(schema.contextChunks).values(chunks.map((chunk) => ({
|
|
232
|
+
id: newId("ccc"),
|
|
233
|
+
itemId,
|
|
234
|
+
itemVersionId,
|
|
235
|
+
ordinal: chunk.ordinal,
|
|
236
|
+
kind: chunk.kind ?? "text",
|
|
237
|
+
text: chunk.text,
|
|
238
|
+
startOffset: chunk.startOffset ?? null,
|
|
239
|
+
endOffset: chunk.endOffset ?? null,
|
|
240
|
+
tokenCount: chunk.tokenCount ?? null,
|
|
241
|
+
metadata: stringifyJson(chunk.metadata),
|
|
242
|
+
createdAt: timestamp,
|
|
243
|
+
ownerEmail: input.ownerEmail,
|
|
244
|
+
orgId: input.orgId,
|
|
245
|
+
})));
|
|
246
|
+
if (input.item.media?.length)
|
|
247
|
+
await tx.insert(schema.contextMedia).values(input.item.media.map((media) => ({
|
|
248
|
+
id: newId("ccm"),
|
|
249
|
+
itemId,
|
|
250
|
+
itemVersionId,
|
|
251
|
+
kind: media.kind,
|
|
252
|
+
mimeType: media.mimeType ?? null,
|
|
253
|
+
accessMode: media.accessMode ?? "public",
|
|
254
|
+
url: media.accessMode === "public" ? (media.url ?? null) : null,
|
|
255
|
+
storageKey: media.storageKey ?? null,
|
|
256
|
+
provenanceUrl: media.provenanceUrl ?? media.url ?? null,
|
|
257
|
+
altText: media.altText ?? null,
|
|
258
|
+
caption: media.caption ?? null,
|
|
259
|
+
captionStatus: media.captionStatus ?? "pending",
|
|
260
|
+
ocrText: media.ocrText ?? null,
|
|
261
|
+
palette: stringifyJson(media.palette ?? []),
|
|
262
|
+
contentHash: media.contentHash ?? null,
|
|
263
|
+
width: media.width ?? null,
|
|
264
|
+
height: media.height ?? null,
|
|
265
|
+
durationMs: media.durationMs ?? null,
|
|
266
|
+
metadata: stringifyJson(media.metadata),
|
|
267
|
+
createdAt: timestamp,
|
|
268
|
+
ownerEmail: input.ownerEmail,
|
|
269
|
+
orgId: input.orgId,
|
|
270
|
+
})));
|
|
271
|
+
if (input.item.edges?.length)
|
|
272
|
+
await tx.insert(schema.contextEdges).values(input.item.edges.map((edge) => ({
|
|
273
|
+
id: newId("cce"),
|
|
274
|
+
fromItemId: itemId,
|
|
275
|
+
fromItemVersionId: itemVersionId,
|
|
276
|
+
toItemId: edge.toItemId ?? null,
|
|
277
|
+
toItemVersionId: edge.toItemVersionId ?? null,
|
|
278
|
+
toExternalId: edge.toExternalId ?? null,
|
|
279
|
+
relation: edge.relation,
|
|
280
|
+
metadata: stringifyJson(edge.metadata),
|
|
281
|
+
createdAt: timestamp,
|
|
282
|
+
ownerEmail: input.ownerEmail,
|
|
283
|
+
orgId: input.orgId,
|
|
284
|
+
})));
|
|
285
|
+
await tx
|
|
286
|
+
.update(schema.contextSources)
|
|
287
|
+
.set({ updatedAt: timestamp })
|
|
288
|
+
.where(eq(schema.contextSources.id, input.sourceId));
|
|
289
|
+
});
|
|
290
|
+
return { itemId, itemVersionId };
|
|
291
|
+
}
|
|
292
|
+
async function readSnapshot(itemId, itemVersionId) {
|
|
293
|
+
const { getDb, schema } = getCreativeContext();
|
|
294
|
+
const [item] = await getDb()
|
|
295
|
+
.select()
|
|
296
|
+
.from(schema.contextItems)
|
|
297
|
+
.where(eq(schema.contextItems.id, itemId))
|
|
298
|
+
.limit(1);
|
|
299
|
+
const [version] = await getDb()
|
|
300
|
+
.select()
|
|
301
|
+
.from(schema.contextItemVersions)
|
|
302
|
+
.where(eq(schema.contextItemVersions.id, itemVersionId))
|
|
303
|
+
.limit(1);
|
|
304
|
+
if (!item || !version)
|
|
305
|
+
throw new Error("Staged context snapshot was not found");
|
|
306
|
+
const [chunks, media, edges] = await Promise.all([
|
|
307
|
+
getDb()
|
|
308
|
+
.select()
|
|
309
|
+
.from(schema.contextChunks)
|
|
310
|
+
.where(eq(schema.contextChunks.itemVersionId, itemVersionId)),
|
|
311
|
+
getDb()
|
|
312
|
+
.select()
|
|
313
|
+
.from(schema.contextMedia)
|
|
314
|
+
.where(eq(schema.contextMedia.itemVersionId, itemVersionId)),
|
|
315
|
+
getDb()
|
|
316
|
+
.select()
|
|
317
|
+
.from(schema.contextEdges)
|
|
318
|
+
.where(eq(schema.contextEdges.fromItemVersionId, itemVersionId)),
|
|
319
|
+
]);
|
|
320
|
+
return {
|
|
321
|
+
externalId: item.externalId,
|
|
322
|
+
kind: item.kind,
|
|
323
|
+
title: version.title,
|
|
324
|
+
canonicalUrl: item.canonicalUrl ?? undefined,
|
|
325
|
+
mimeType: version.mimeType ?? undefined,
|
|
326
|
+
content: version.content,
|
|
327
|
+
summary: version.summary ?? undefined,
|
|
328
|
+
contentHash: version.contentHash,
|
|
329
|
+
sourceModifiedAt: version.sourceModifiedAt ?? undefined,
|
|
330
|
+
sourceVersion: version.sourceVersion ?? undefined,
|
|
331
|
+
rawSnapshotBlobRef: version.rawSnapshotBlobRef ?? undefined,
|
|
332
|
+
parseStatus: version.parseStatus,
|
|
333
|
+
parseError: version.parseError ?? undefined,
|
|
334
|
+
upstreamAccess: item.upstreamAccess,
|
|
335
|
+
curationStatus: "included",
|
|
336
|
+
curationRank: item.curationRank,
|
|
337
|
+
tags: parseJson(item.tags, []),
|
|
338
|
+
colors: parseJson(item.colors, []),
|
|
339
|
+
provenance: parseJson(item.provenance, {}),
|
|
340
|
+
thumbnailBlobRef: item.thumbnailBlobRef ?? undefined,
|
|
341
|
+
metadata: parseJson(version.metadata, {}),
|
|
342
|
+
chunks: chunks.map((row) => ({
|
|
343
|
+
ordinal: row.ordinal,
|
|
344
|
+
kind: row.kind,
|
|
345
|
+
text: row.text,
|
|
346
|
+
startOffset: row.startOffset ?? undefined,
|
|
347
|
+
endOffset: row.endOffset ?? undefined,
|
|
348
|
+
tokenCount: row.tokenCount ?? undefined,
|
|
349
|
+
metadata: parseJson(row.metadata, {}),
|
|
350
|
+
})),
|
|
351
|
+
media: media.map((row) => ({
|
|
352
|
+
kind: row.kind,
|
|
353
|
+
mimeType: row.mimeType ?? undefined,
|
|
354
|
+
accessMode: row.accessMode,
|
|
355
|
+
url: row.url ?? undefined,
|
|
356
|
+
storageKey: row.storageKey ?? undefined,
|
|
357
|
+
provenanceUrl: row.provenanceUrl ?? undefined,
|
|
358
|
+
altText: row.altText ?? undefined,
|
|
359
|
+
caption: row.caption ?? undefined,
|
|
360
|
+
captionStatus: row.captionStatus,
|
|
361
|
+
ocrText: row.ocrText ?? undefined,
|
|
362
|
+
palette: parseJson(row.palette, []),
|
|
363
|
+
contentHash: row.contentHash ?? undefined,
|
|
364
|
+
width: row.width ?? undefined,
|
|
365
|
+
height: row.height ?? undefined,
|
|
366
|
+
durationMs: row.durationMs ?? undefined,
|
|
367
|
+
metadata: parseJson(row.metadata, {}),
|
|
368
|
+
})),
|
|
369
|
+
edges: edges.map((row) => ({
|
|
370
|
+
relation: row.relation,
|
|
371
|
+
...(row.toItemId ? { toItemId: row.toItemId } : {}),
|
|
372
|
+
...(row.toItemVersionId ? { toItemVersionId: row.toItemVersionId } : {}),
|
|
373
|
+
...(row.toExternalId ? { toExternalId: row.toExternalId } : {}),
|
|
374
|
+
metadata: parseJson(row.metadata, {}),
|
|
375
|
+
})),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
export async function createCreativeContext(input) {
|
|
379
|
+
const { getDb, schema } = getCreativeContext();
|
|
380
|
+
const actor = requireActor();
|
|
381
|
+
const timestamp = nowIso();
|
|
382
|
+
if (actor.orgId)
|
|
383
|
+
await assertCurrentRequestUserIsOrgAdmin(actor.orgId);
|
|
384
|
+
const defaultScopeKey = input.kind === "default" ? defaultContextScopeKey(actor) : null;
|
|
385
|
+
if (input.kind === "default") {
|
|
386
|
+
const existing = await getDb()
|
|
387
|
+
.select({ id: schema.creativeContexts.id })
|
|
388
|
+
.from(schema.creativeContexts)
|
|
389
|
+
.where(eq(schema.creativeContexts.defaultScopeKey, defaultScopeKey))
|
|
390
|
+
.limit(1);
|
|
391
|
+
if (existing[0])
|
|
392
|
+
return getCreativeContextById(existing[0].id);
|
|
393
|
+
}
|
|
394
|
+
const id = newId("ccx"), stagingSourceId = newId("ccs"), publishedSourceId = newId("ccs");
|
|
395
|
+
try {
|
|
396
|
+
await getDb().transaction(async (tx) => {
|
|
397
|
+
for (const [sourceId, name, purpose] of [
|
|
398
|
+
[stagingSourceId, `${input.name} staging`, "staging"],
|
|
399
|
+
[publishedSourceId, `${input.name} published`, "published"],
|
|
400
|
+
])
|
|
401
|
+
await tx.insert(schema.contextSources).values({
|
|
402
|
+
id: sourceId,
|
|
403
|
+
name,
|
|
404
|
+
kind: "native-app",
|
|
405
|
+
config: stringifyJson({ governedContextId: id, purpose }),
|
|
406
|
+
upstreamAccess: "available",
|
|
407
|
+
status: "active",
|
|
408
|
+
healthStatus: "healthy",
|
|
409
|
+
itemCount: 0,
|
|
410
|
+
restrictedItemCount: 0,
|
|
411
|
+
createdAt: timestamp,
|
|
412
|
+
updatedAt: timestamp,
|
|
413
|
+
ownerEmail: actor.ownerEmail,
|
|
414
|
+
orgId: actor.orgId,
|
|
415
|
+
visibility: "private",
|
|
416
|
+
});
|
|
417
|
+
await tx.insert(schema.creativeContexts).values({
|
|
418
|
+
id,
|
|
419
|
+
name: input.name,
|
|
420
|
+
description: input.description ?? null,
|
|
421
|
+
kind: input.kind,
|
|
422
|
+
defaultScopeKey,
|
|
423
|
+
brandProfileId: input.brandProfileId ?? null,
|
|
424
|
+
stagingSourceId,
|
|
425
|
+
publishedSourceId,
|
|
426
|
+
approvalPolicy: input.approvalPolicy ?? "open",
|
|
427
|
+
archivedAt: null,
|
|
428
|
+
createdAt: timestamp,
|
|
429
|
+
updatedAt: timestamp,
|
|
430
|
+
ownerEmail: actor.ownerEmail,
|
|
431
|
+
orgId: actor.orgId,
|
|
432
|
+
visibility: actor.orgId ? "org" : "private",
|
|
433
|
+
});
|
|
434
|
+
await appendAudit(tx, id, "create", {
|
|
435
|
+
kind: input.kind,
|
|
436
|
+
approvalPolicy: input.approvalPolicy ?? "open",
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (input.kind === "default") {
|
|
442
|
+
const existing = await getDb()
|
|
443
|
+
.select({ id: schema.creativeContexts.id })
|
|
444
|
+
.from(schema.creativeContexts)
|
|
445
|
+
.where(eq(schema.creativeContexts.defaultScopeKey, defaultScopeKey))
|
|
446
|
+
.limit(1);
|
|
447
|
+
if (existing[0])
|
|
448
|
+
return getCreativeContextById(existing[0].id);
|
|
449
|
+
}
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
return getCreativeContextById(id);
|
|
453
|
+
}
|
|
454
|
+
/** Idempotently establishes the actor's governed Default with the currently usable corpus. */
|
|
455
|
+
export async function ensureDefaultCreativeContext() {
|
|
456
|
+
const { getDb, schema } = getCreativeContext();
|
|
457
|
+
const actor = requireActor();
|
|
458
|
+
const existing = await getDb()
|
|
459
|
+
.select()
|
|
460
|
+
.from(schema.creativeContexts)
|
|
461
|
+
.where(eq(schema.creativeContexts.defaultScopeKey, defaultContextScopeKey(actor)))
|
|
462
|
+
.limit(1);
|
|
463
|
+
if (existing[0])
|
|
464
|
+
return getCreativeContextById(existing[0].id);
|
|
465
|
+
if (actor.orgId && !(await currentRequestUserIsOrgAdmin(actor.orgId)))
|
|
466
|
+
return null;
|
|
467
|
+
const profileRows = await getDb()
|
|
468
|
+
.select({ id: schema.brandProfiles.id })
|
|
469
|
+
.from(schema.brandProfiles)
|
|
470
|
+
.innerJoin(schema.brandDnaVersions, eq(schema.brandDnaVersions.profileId, schema.brandProfiles.id))
|
|
471
|
+
.where(and(accessFilter(schema.brandProfiles, schema.brandProfileShares), eq(schema.brandDnaVersions.status, "published")))
|
|
472
|
+
.limit(1);
|
|
473
|
+
const created = await createCreativeContext({
|
|
474
|
+
name: "Default",
|
|
475
|
+
kind: "default",
|
|
476
|
+
brandProfileId: profileRows[0]?.id ?? null,
|
|
477
|
+
approvalPolicy: "open",
|
|
478
|
+
});
|
|
479
|
+
if (!created)
|
|
480
|
+
return null;
|
|
481
|
+
const corpus = await getDb()
|
|
482
|
+
.select({
|
|
483
|
+
itemId: schema.contextItems.id,
|
|
484
|
+
itemVersionId: schema.contextItems.currentVersionId,
|
|
485
|
+
sourceId: schema.contextSources.id,
|
|
486
|
+
externalId: schema.contextItems.externalId,
|
|
487
|
+
rank: schema.contextItems.curationRank,
|
|
488
|
+
})
|
|
489
|
+
.from(schema.contextItems)
|
|
490
|
+
.innerJoin(schema.contextSources, eq(schema.contextSources.id, schema.contextItems.sourceId))
|
|
491
|
+
.where(and(accessFilter(schema.contextSources, schema.contextSourceShares), ...(created.visibility === "org"
|
|
492
|
+
? [inArray(schema.contextSources.visibility, ["org", "public"])]
|
|
493
|
+
: []), eq(schema.contextItems.curationStatus, "included"), eq(schema.contextItems.status, "active"), eq(schema.contextSources.status, "active"), eq(schema.contextSources.upstreamAccess, "available")))
|
|
494
|
+
.limit(5_000);
|
|
495
|
+
const timestamp = nowIso();
|
|
496
|
+
if (corpus.length)
|
|
497
|
+
await getDb().transaction(async (tx) => {
|
|
498
|
+
await tx.insert(schema.creativeContextMemberships).values(corpus.map((item) => ({
|
|
499
|
+
id: newId("ccmbr"),
|
|
500
|
+
contextId: created.id,
|
|
501
|
+
artifactKey: `${item.sourceId}:${item.externalId}`,
|
|
502
|
+
publishedItemId: item.itemId,
|
|
503
|
+
publishedItemVersionId: item.itemVersionId,
|
|
504
|
+
pendingSubmissionId: null,
|
|
505
|
+
rank: item.rank === "canonical" || item.rank === "exemplar"
|
|
506
|
+
? item.rank
|
|
507
|
+
: "normal",
|
|
508
|
+
purpose: "Backfilled accessible corpus",
|
|
509
|
+
status: "active",
|
|
510
|
+
createdAt: timestamp,
|
|
511
|
+
updatedAt: timestamp,
|
|
512
|
+
ownerEmail: actor.ownerEmail,
|
|
513
|
+
orgId: actor.orgId,
|
|
514
|
+
})));
|
|
515
|
+
await appendAudit(tx, created.id, "backfill-default", {
|
|
516
|
+
members: corpus.length,
|
|
517
|
+
brandProfileId: profileRows[0]?.id ?? null,
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
return getCreativeContextById(created.id);
|
|
521
|
+
}
|
|
522
|
+
export async function getCreativeContextById(contextId) {
|
|
523
|
+
const access = await resolveAccess("creative-context", contextId);
|
|
524
|
+
if (!access)
|
|
525
|
+
return null;
|
|
526
|
+
const { getDb, schema } = getCreativeContext();
|
|
527
|
+
const [membershipCount] = await getDb()
|
|
528
|
+
.select({ value: count() })
|
|
529
|
+
.from(schema.creativeContextMemberships)
|
|
530
|
+
.where(and(eq(schema.creativeContextMemberships.contextId, contextId), eq(schema.creativeContextMemberships.status, "active")));
|
|
531
|
+
return mapContext(access.resource, Number(membershipCount?.value ?? 0), access.role);
|
|
532
|
+
}
|
|
533
|
+
export async function getCreativeContextAppBinding(appId) {
|
|
534
|
+
const { getDb, schema } = getCreativeContext();
|
|
535
|
+
const actor = requireActor();
|
|
536
|
+
const scope = actor.orgId
|
|
537
|
+
? eq(schema.creativeContextAppBindings.orgId, actor.orgId)
|
|
538
|
+
: isNull(schema.creativeContextAppBindings.orgId);
|
|
539
|
+
const rows = await getDb()
|
|
540
|
+
.select({ contextId: schema.creativeContextAppBindings.contextId })
|
|
541
|
+
.from(schema.creativeContextAppBindings)
|
|
542
|
+
.where(and(eq(schema.creativeContextAppBindings.appId, appId), scope, ...(actor.orgId
|
|
543
|
+
? []
|
|
544
|
+
: [
|
|
545
|
+
eq(schema.creativeContextAppBindings.ownerEmail, actor.ownerEmail),
|
|
546
|
+
])))
|
|
547
|
+
.orderBy(asc(schema.creativeContextAppBindings.updatedAt))
|
|
548
|
+
.limit(1);
|
|
549
|
+
return rows[0] ? getCreativeContextById(rows[0].contextId) : null;
|
|
550
|
+
}
|
|
551
|
+
export async function listCreativeContexts(input) {
|
|
552
|
+
await ensureDefaultCreativeContext();
|
|
553
|
+
const { getDb, schema } = getCreativeContext();
|
|
554
|
+
const filters = [
|
|
555
|
+
accessFilter(schema.creativeContexts, schema.creativeContextShares),
|
|
556
|
+
];
|
|
557
|
+
if (input.cursor)
|
|
558
|
+
filters.push(gt(schema.creativeContexts.id, input.cursor));
|
|
559
|
+
if (!input.includeArchived)
|
|
560
|
+
filters.push(isNull(schema.creativeContexts.archivedAt));
|
|
561
|
+
const rows = await getDb()
|
|
562
|
+
.select()
|
|
563
|
+
.from(schema.creativeContexts)
|
|
564
|
+
.where(and(...filters))
|
|
565
|
+
.orderBy(asc(schema.creativeContexts.id))
|
|
566
|
+
.limit(input.limit + 1);
|
|
567
|
+
const page = rows.slice(0, input.limit);
|
|
568
|
+
const counts = page.length
|
|
569
|
+
? await getDb()
|
|
570
|
+
.select({
|
|
571
|
+
contextId: schema.creativeContextMemberships.contextId,
|
|
572
|
+
id: schema.creativeContextMemberships.id,
|
|
573
|
+
})
|
|
574
|
+
.from(schema.creativeContextMemberships)
|
|
575
|
+
.where(and(inArray(schema.creativeContextMemberships.contextId, page.map((row) => row.id)), eq(schema.creativeContextMemberships.status, "active")))
|
|
576
|
+
: [];
|
|
577
|
+
const byContext = new Map();
|
|
578
|
+
for (const row of counts)
|
|
579
|
+
byContext.set(row.contextId, (byContext.get(row.contextId) ?? 0) + 1);
|
|
580
|
+
const accessById = new Map((await Promise.all(page.map(async (row) => [row.id, await resolveAccess("creative-context", row.id)]))).flatMap(([id, access]) => (access ? [[id, access]] : [])));
|
|
581
|
+
return {
|
|
582
|
+
contexts: page.flatMap((row) => {
|
|
583
|
+
const access = accessById.get(row.id);
|
|
584
|
+
return access
|
|
585
|
+
? [mapContext(row, byContext.get(row.id) ?? 0, access.role)]
|
|
586
|
+
: [];
|
|
587
|
+
}),
|
|
588
|
+
nextCursor: rows.length > input.limit ? page.at(-1)?.id : undefined,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
export async function updateCreativeContext(contextId, patch) {
|
|
592
|
+
await assertContextRole(contextId, "admin");
|
|
593
|
+
const { getDb, schema } = getCreativeContext();
|
|
594
|
+
const values = { ...patch, updatedAt: nowIso() };
|
|
595
|
+
await getDb().transaction(async (tx) => {
|
|
596
|
+
await tx
|
|
597
|
+
.update(schema.creativeContexts)
|
|
598
|
+
.set(values)
|
|
599
|
+
.where(eq(schema.creativeContexts.id, contextId));
|
|
600
|
+
await appendAudit(tx, contextId, "update", { fields: Object.keys(patch) });
|
|
601
|
+
});
|
|
602
|
+
return getCreativeContextById(contextId);
|
|
603
|
+
}
|
|
604
|
+
export async function archiveCreativeContext(contextId) {
|
|
605
|
+
await assertContextRole(contextId, "admin");
|
|
606
|
+
const { getDb, schema } = getCreativeContext();
|
|
607
|
+
const [context] = await getDb()
|
|
608
|
+
.select({ kind: schema.creativeContexts.kind })
|
|
609
|
+
.from(schema.creativeContexts)
|
|
610
|
+
.where(eq(schema.creativeContexts.id, contextId))
|
|
611
|
+
.limit(1);
|
|
612
|
+
if (context?.kind === "default")
|
|
613
|
+
throw new Error("The Default Creative Context cannot be archived");
|
|
614
|
+
await getDb()
|
|
615
|
+
.update(schema.creativeContexts)
|
|
616
|
+
.set({ archivedAt: nowIso(), updatedAt: nowIso() })
|
|
617
|
+
.where(eq(schema.creativeContexts.id, contextId));
|
|
618
|
+
return getCreativeContextById(contextId);
|
|
619
|
+
}
|
|
620
|
+
export async function setCreativeContextAppDefault(contextId, appId) {
|
|
621
|
+
const access = await assertContextRole(contextId, "admin");
|
|
622
|
+
if (access.resource.orgId)
|
|
623
|
+
await assertCurrentRequestUserIsOrgAdmin(access.resource.orgId);
|
|
624
|
+
const { getDb, schema } = getCreativeContext();
|
|
625
|
+
const actor = requireActor();
|
|
626
|
+
const timestamp = nowIso();
|
|
627
|
+
await getDb().transaction(async (tx) => {
|
|
628
|
+
await tx
|
|
629
|
+
.delete(schema.creativeContextAppBindings)
|
|
630
|
+
.where(and(eq(schema.creativeContextAppBindings.appId, appId), actor.orgId
|
|
631
|
+
? eq(schema.creativeContextAppBindings.orgId, actor.orgId)
|
|
632
|
+
: and(isNull(schema.creativeContextAppBindings.orgId), eq(schema.creativeContextAppBindings.ownerEmail, actor.ownerEmail))));
|
|
633
|
+
await tx.insert(schema.creativeContextAppBindings).values({
|
|
634
|
+
id: newId("ccab"),
|
|
635
|
+
appId,
|
|
636
|
+
contextId,
|
|
637
|
+
createdAt: timestamp,
|
|
638
|
+
updatedAt: timestamp,
|
|
639
|
+
ownerEmail: actor.ownerEmail,
|
|
640
|
+
orgId: actor.orgId,
|
|
641
|
+
});
|
|
642
|
+
await appendAudit(tx, contextId, "set-app-default", { appId });
|
|
643
|
+
});
|
|
644
|
+
return getCreativeContextById(contextId);
|
|
645
|
+
}
|
|
646
|
+
export async function listContextMemberships(input) {
|
|
647
|
+
const access = await assertContextRole(input.contextId, "viewer");
|
|
648
|
+
const { getDb, schema } = getCreativeContext();
|
|
649
|
+
const actor = requireActor();
|
|
650
|
+
const canReview = access.role === "owner" || access.role === "admin";
|
|
651
|
+
const filters = [
|
|
652
|
+
eq(schema.creativeContextMemberships.contextId, input.contextId),
|
|
653
|
+
];
|
|
654
|
+
if (input.status)
|
|
655
|
+
filters.push(eq(schema.creativeContextMemberships.status, input.status));
|
|
656
|
+
if (input.cursor)
|
|
657
|
+
filters.push(gt(schema.creativeContextMemberships.id, input.cursor));
|
|
658
|
+
if (!canReview)
|
|
659
|
+
filters.push(or(isNotNull(schema.creativeContextMemberships.publishedItemId), eq(schema.creativeContextSubmissions.submittedBy, actor.ownerEmail)));
|
|
660
|
+
const rows = await getDb()
|
|
661
|
+
.select({
|
|
662
|
+
membership: schema.creativeContextMemberships,
|
|
663
|
+
pendingSubmission: schema.creativeContextSubmissions,
|
|
664
|
+
})
|
|
665
|
+
.from(schema.creativeContextMemberships)
|
|
666
|
+
.leftJoin(schema.creativeContextSubmissions, eq(schema.creativeContextSubmissions.id, schema.creativeContextMemberships.pendingSubmissionId))
|
|
667
|
+
.where(and(...filters))
|
|
668
|
+
.orderBy(asc(schema.creativeContextMemberships.id))
|
|
669
|
+
.limit(input.limit + 1);
|
|
670
|
+
const page = rows.slice(0, input.limit);
|
|
671
|
+
const canViewPendingSubmission = (submission) => Boolean(submission && (canReview || submission.submittedBy === actor.ownerEmail));
|
|
672
|
+
const publishedVersionIds = page.flatMap(({ membership }) => membership.publishedItemVersionId
|
|
673
|
+
? [membership.publishedItemVersionId]
|
|
674
|
+
: []);
|
|
675
|
+
const pendingVersionIds = page.flatMap(({ pendingSubmission }) => canViewPendingSubmission(pendingSubmission) &&
|
|
676
|
+
pendingSubmission.stagingItemVersionId
|
|
677
|
+
? [pendingSubmission.stagingItemVersionId]
|
|
678
|
+
: []);
|
|
679
|
+
const previewVersionIds = [
|
|
680
|
+
...new Set([...publishedVersionIds, ...pendingVersionIds]),
|
|
681
|
+
];
|
|
682
|
+
const [previewItems, previewMedia] = previewVersionIds.length
|
|
683
|
+
? await Promise.all([
|
|
684
|
+
getDb()
|
|
685
|
+
.select({
|
|
686
|
+
id: schema.contextItems.id,
|
|
687
|
+
itemVersionId: schema.contextItemVersions.id,
|
|
688
|
+
title: schema.contextItemVersions.title,
|
|
689
|
+
kind: schema.contextItems.kind,
|
|
690
|
+
canonicalUrl: schema.contextItems.canonicalUrl,
|
|
691
|
+
status: schema.contextItems.status,
|
|
692
|
+
sourceModifiedAt: schema.contextItemVersions.sourceModifiedAt,
|
|
693
|
+
metadata: schema.contextItemVersions.metadata,
|
|
694
|
+
})
|
|
695
|
+
.from(schema.contextItemVersions)
|
|
696
|
+
.innerJoin(schema.contextItems, eq(schema.contextItems.id, schema.contextItemVersions.itemId))
|
|
697
|
+
.where(inArray(schema.contextItemVersions.id, previewVersionIds)),
|
|
698
|
+
getDb()
|
|
699
|
+
.select({
|
|
700
|
+
id: schema.contextMedia.id,
|
|
701
|
+
itemVersionId: schema.contextMedia.itemVersionId,
|
|
702
|
+
kind: schema.contextMedia.kind,
|
|
703
|
+
mimeType: schema.contextMedia.mimeType,
|
|
704
|
+
})
|
|
705
|
+
.from(schema.contextMedia)
|
|
706
|
+
.where(inArray(schema.contextMedia.itemVersionId, previewVersionIds)),
|
|
707
|
+
])
|
|
708
|
+
: [[], []];
|
|
709
|
+
const mediaByVersion = new Map();
|
|
710
|
+
for (const media of previewMedia) {
|
|
711
|
+
const list = mediaByVersion.get(media.itemVersionId) ?? [];
|
|
712
|
+
list.push({
|
|
713
|
+
id: media.id,
|
|
714
|
+
kind: media.kind,
|
|
715
|
+
mimeType: media.mimeType ?? null,
|
|
716
|
+
url: creativeContextMediaUrl({ mediaId: media.id }),
|
|
717
|
+
});
|
|
718
|
+
mediaByVersion.set(media.itemVersionId, list);
|
|
719
|
+
}
|
|
720
|
+
const previewByVersion = new Map(previewItems.map((item) => {
|
|
721
|
+
const preview = sanitizePublicMetadata(parseJson(item.metadata, {}).preview);
|
|
722
|
+
return [
|
|
723
|
+
item.itemVersionId,
|
|
724
|
+
{
|
|
725
|
+
id: item.id,
|
|
726
|
+
itemVersionId: item.itemVersionId,
|
|
727
|
+
title: item.title,
|
|
728
|
+
kind: item.kind,
|
|
729
|
+
canonicalUrl: typeof sanitizePublicMetadata(item.canonicalUrl) === "string"
|
|
730
|
+
? sanitizePublicMetadata(item.canonicalUrl)
|
|
731
|
+
: null,
|
|
732
|
+
status: item.status,
|
|
733
|
+
sourceModifiedAt: item.sourceModifiedAt ?? null,
|
|
734
|
+
media: mediaByVersion.get(item.itemVersionId) ?? [],
|
|
735
|
+
...(preview && typeof preview === "object" && !Array.isArray(preview)
|
|
736
|
+
? { preview: preview }
|
|
737
|
+
: {}),
|
|
738
|
+
},
|
|
739
|
+
];
|
|
740
|
+
}));
|
|
741
|
+
return {
|
|
742
|
+
memberships: page.map(({ membership, pendingSubmission }) => {
|
|
743
|
+
const canViewPending = canViewPendingSubmission(pendingSubmission);
|
|
744
|
+
return {
|
|
745
|
+
...mapMembership(canViewPending
|
|
746
|
+
? membership
|
|
747
|
+
: { ...membership, pendingSubmissionId: null }),
|
|
748
|
+
publishedItem: membership.publishedItemVersionId
|
|
749
|
+
? (previewByVersion.get(membership.publishedItemVersionId) ?? null)
|
|
750
|
+
: null,
|
|
751
|
+
pendingSubmission: canViewPending && pendingSubmission
|
|
752
|
+
? {
|
|
753
|
+
...mapSubmission(pendingSubmission),
|
|
754
|
+
proposedItem: pendingSubmission.stagingItemVersionId
|
|
755
|
+
? (previewByVersion.get(pendingSubmission.stagingItemVersionId) ?? null)
|
|
756
|
+
: null,
|
|
757
|
+
}
|
|
758
|
+
: null,
|
|
759
|
+
};
|
|
760
|
+
}),
|
|
761
|
+
nextCursor: rows.length > input.limit ? page.at(-1)?.membership.id : undefined,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Resolves private media for a pending submission without making the staged
|
|
766
|
+
* item generally readable. This is intentionally server-only: callers must
|
|
767
|
+
* already have an authenticated request context and can only read the exact
|
|
768
|
+
* staged version they submitted or are allowed to review.
|
|
769
|
+
*/
|
|
770
|
+
export async function readPendingCreativeContextMedia(input) {
|
|
771
|
+
const { getDb, schema } = getCreativeContext();
|
|
772
|
+
const actor = requireActor();
|
|
773
|
+
let itemId = input.itemId;
|
|
774
|
+
let itemVersionId = input.itemVersionId;
|
|
775
|
+
let media = null;
|
|
776
|
+
if (input.mediaId) {
|
|
777
|
+
const rows = await getDb()
|
|
778
|
+
.select({
|
|
779
|
+
id: schema.contextMedia.id,
|
|
780
|
+
itemId: schema.contextMedia.itemId,
|
|
781
|
+
itemVersionId: schema.contextMedia.itemVersionId,
|
|
782
|
+
storageKey: schema.contextMedia.storageKey,
|
|
783
|
+
mimeType: schema.contextMedia.mimeType,
|
|
784
|
+
})
|
|
785
|
+
.from(schema.contextMedia)
|
|
786
|
+
.where(eq(schema.contextMedia.id, input.mediaId))
|
|
787
|
+
.limit(1);
|
|
788
|
+
media = rows[0] ?? null;
|
|
789
|
+
if (!media)
|
|
790
|
+
return null;
|
|
791
|
+
if ((itemId && itemId !== media.itemId) ||
|
|
792
|
+
(itemVersionId && itemVersionId !== media.itemVersionId)) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
itemId = media.itemId;
|
|
796
|
+
itemVersionId = media.itemVersionId;
|
|
797
|
+
}
|
|
798
|
+
if (!itemId || !itemVersionId)
|
|
799
|
+
return null;
|
|
800
|
+
const submissions = await getDb()
|
|
801
|
+
.select({
|
|
802
|
+
contextId: schema.creativeContextSubmissions.contextId,
|
|
803
|
+
submittedBy: schema.creativeContextSubmissions.submittedBy,
|
|
804
|
+
})
|
|
805
|
+
.from(schema.creativeContextSubmissions)
|
|
806
|
+
.where(and(eq(schema.creativeContextSubmissions.status, "pending"), eq(schema.creativeContextSubmissions.stagingItemId, itemId), eq(schema.creativeContextSubmissions.stagingItemVersionId, itemVersionId)))
|
|
807
|
+
.limit(1);
|
|
808
|
+
const submission = submissions[0];
|
|
809
|
+
if (!submission)
|
|
810
|
+
return null;
|
|
811
|
+
const access = await resolveAccess("creative-context", submission.contextId);
|
|
812
|
+
if (!access)
|
|
813
|
+
return null;
|
|
814
|
+
const canReview = access.role === "owner" || access.role === "admin";
|
|
815
|
+
if (!canReview && submission.submittedBy !== actor.ownerEmail)
|
|
816
|
+
return null;
|
|
817
|
+
if (media) {
|
|
818
|
+
return {
|
|
819
|
+
itemId,
|
|
820
|
+
itemVersionId,
|
|
821
|
+
mediaId: media.id,
|
|
822
|
+
storageKey: media.storageKey,
|
|
823
|
+
mimeType: media.mimeType,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const rows = await getDb()
|
|
827
|
+
.select({
|
|
828
|
+
thumbnailBlobRef: schema.contextItems.thumbnailBlobRef,
|
|
829
|
+
mimeType: schema.contextItemVersions.mimeType,
|
|
830
|
+
})
|
|
831
|
+
.from(schema.contextItemVersions)
|
|
832
|
+
.innerJoin(schema.contextItems, eq(schema.contextItems.id, schema.contextItemVersions.itemId))
|
|
833
|
+
.where(and(eq(schema.contextItems.id, itemId), eq(schema.contextItemVersions.id, itemVersionId)))
|
|
834
|
+
.limit(1);
|
|
835
|
+
const item = rows[0];
|
|
836
|
+
return item
|
|
837
|
+
? {
|
|
838
|
+
itemId,
|
|
839
|
+
itemVersionId,
|
|
840
|
+
mediaId: null,
|
|
841
|
+
storageKey: item.thumbnailBlobRef,
|
|
842
|
+
mimeType: item.mimeType,
|
|
843
|
+
}
|
|
844
|
+
: null;
|
|
845
|
+
}
|
|
846
|
+
async function resolveSubmissionItem(input) {
|
|
847
|
+
if (input.nativeResource) {
|
|
848
|
+
const captured = await captureNativeCreativeResource(input.nativeResource);
|
|
849
|
+
if (!captured.items.length)
|
|
850
|
+
throw new Error("Native capture returned no artifacts");
|
|
851
|
+
return {
|
|
852
|
+
artifactKey: captured.artifactKey,
|
|
853
|
+
items: captured.items,
|
|
854
|
+
privateMetadata: {
|
|
855
|
+
...(captured.privateMetadata ?? {}),
|
|
856
|
+
nativeResource: input.nativeResource,
|
|
857
|
+
},
|
|
858
|
+
sourceAccess: captured.source.access ?? null,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
if (!input.itemId)
|
|
862
|
+
throw new Error("itemId or nativeResource is required");
|
|
863
|
+
const detail = await getCreativeContextItem(input.itemId, input.itemVersionId);
|
|
864
|
+
if (!detail)
|
|
865
|
+
throw new Error("Context item version not found or not accessible");
|
|
866
|
+
const sourceAccess = await resolveAccess("creative-context-source", detail.item.sourceId);
|
|
867
|
+
const children = await Promise.all(detail.edges
|
|
868
|
+
.filter((edge) => edge.relation === "contains-native-child" &&
|
|
869
|
+
typeof edge.toItemId === "string")
|
|
870
|
+
.map((edge) => getCreativeContextItem(edge.toItemId, edge.toItemVersionId ?? undefined)));
|
|
871
|
+
return {
|
|
872
|
+
artifactKey: `${detail.item.sourceId}:${detail.item.externalId}`,
|
|
873
|
+
items: [
|
|
874
|
+
normalizedFromDetail(detail),
|
|
875
|
+
...children.flatMap((child) => child ? [normalizedFromDetail(child)] : []),
|
|
876
|
+
],
|
|
877
|
+
privateMetadata: {},
|
|
878
|
+
sourceAccess: sourceAccess
|
|
879
|
+
? {
|
|
880
|
+
visibility: sourceAccess.resource.visibility,
|
|
881
|
+
canManage: sourceAccess.role === "owner" || sourceAccess.role === "admin",
|
|
882
|
+
}
|
|
883
|
+
: null,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function visibilityRank(visibility) {
|
|
887
|
+
return visibility === "public" ? 2 : visibility === "org" ? 1 : 0;
|
|
888
|
+
}
|
|
889
|
+
async function approveSubmission(context, membership, submission) {
|
|
890
|
+
const { getDb, schema } = getCreativeContext();
|
|
891
|
+
const actor = requireActor();
|
|
892
|
+
const staged = await readSnapshot(submission.stagingItemId, submission.stagingItemVersionId);
|
|
893
|
+
const published = await writeSnapshot({
|
|
894
|
+
sourceId: context.publishedSourceId,
|
|
895
|
+
artifactKey: submission.artifactKey,
|
|
896
|
+
item: staged,
|
|
897
|
+
ownerEmail: context.ownerEmail,
|
|
898
|
+
orgId: context.orgId ?? null,
|
|
899
|
+
});
|
|
900
|
+
const privateMetadata = parseJson(submission.privateMetadata, {});
|
|
901
|
+
const publishedChildren = [];
|
|
902
|
+
for (const child of Array.isArray(privateMetadata.stagedChildren)
|
|
903
|
+
? privateMetadata.stagedChildren
|
|
904
|
+
: []) {
|
|
905
|
+
if (typeof child.artifactKey !== "string" ||
|
|
906
|
+
typeof child.itemId !== "string" ||
|
|
907
|
+
typeof child.itemVersionId !== "string")
|
|
908
|
+
continue;
|
|
909
|
+
const publishedChild = await writeSnapshot({
|
|
910
|
+
sourceId: context.publishedSourceId,
|
|
911
|
+
artifactKey: child.artifactKey,
|
|
912
|
+
item: await readSnapshot(child.itemId, child.itemVersionId),
|
|
913
|
+
ownerEmail: context.ownerEmail,
|
|
914
|
+
orgId: context.orgId ?? null,
|
|
915
|
+
});
|
|
916
|
+
publishedChildren.push({
|
|
917
|
+
artifactKey: child.artifactKey,
|
|
918
|
+
...publishedChild,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
for (const child of publishedChildren) {
|
|
922
|
+
await getDb()
|
|
923
|
+
.update(schema.contextEdges)
|
|
924
|
+
.set({ toItemId: child.itemId, toItemVersionId: child.itemVersionId })
|
|
925
|
+
.where(and(eq(schema.contextEdges.fromItemVersionId, published.itemVersionId), eq(schema.contextEdges.toExternalId, child.artifactKey)));
|
|
926
|
+
}
|
|
927
|
+
const timestamp = nowIso();
|
|
928
|
+
await getDb().transaction(async (tx) => {
|
|
929
|
+
await tx
|
|
930
|
+
.update(schema.creativeContextSubmissions)
|
|
931
|
+
.set({
|
|
932
|
+
status: "approved",
|
|
933
|
+
publishedItemId: published.itemId,
|
|
934
|
+
publishedItemVersionId: published.itemVersionId,
|
|
935
|
+
reviewedBy: actor.ownerEmail,
|
|
936
|
+
reviewedAt: timestamp,
|
|
937
|
+
})
|
|
938
|
+
.where(eq(schema.creativeContextSubmissions.id, submission.id));
|
|
939
|
+
await tx
|
|
940
|
+
.update(schema.creativeContextMemberships)
|
|
941
|
+
.set({
|
|
942
|
+
publishedItemId: published.itemId,
|
|
943
|
+
publishedItemVersionId: published.itemVersionId,
|
|
944
|
+
pendingSubmissionId: null,
|
|
945
|
+
status: "active",
|
|
946
|
+
updatedAt: timestamp,
|
|
947
|
+
})
|
|
948
|
+
.where(eq(schema.creativeContextMemberships.id, membership.id));
|
|
949
|
+
for (const child of publishedChildren) {
|
|
950
|
+
const existingChild = await tx
|
|
951
|
+
.select({ id: schema.creativeContextMemberships.id })
|
|
952
|
+
.from(schema.creativeContextMemberships)
|
|
953
|
+
.where(and(eq(schema.creativeContextMemberships.contextId, context.id), eq(schema.creativeContextMemberships.artifactKey, child.artifactKey)))
|
|
954
|
+
.limit(1);
|
|
955
|
+
const values = {
|
|
956
|
+
publishedItemId: child.itemId,
|
|
957
|
+
publishedItemVersionId: child.itemVersionId,
|
|
958
|
+
pendingSubmissionId: null,
|
|
959
|
+
status: "active",
|
|
960
|
+
updatedAt: timestamp,
|
|
961
|
+
};
|
|
962
|
+
if (existingChild[0]) {
|
|
963
|
+
await tx
|
|
964
|
+
.update(schema.creativeContextMemberships)
|
|
965
|
+
.set(values)
|
|
966
|
+
.where(eq(schema.creativeContextMemberships.id, existingChild[0].id));
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
await tx.insert(schema.creativeContextMemberships).values({
|
|
970
|
+
id: newId("ccmbr"),
|
|
971
|
+
contextId: context.id,
|
|
972
|
+
artifactKey: child.artifactKey,
|
|
973
|
+
...values,
|
|
974
|
+
rank: "normal",
|
|
975
|
+
purpose: "Native artifact child",
|
|
976
|
+
createdAt: timestamp,
|
|
977
|
+
ownerEmail: context.ownerEmail,
|
|
978
|
+
orgId: context.orgId ?? null,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
await tx.insert(schema.creativeContextPublishedSnapshots).values([
|
|
983
|
+
{
|
|
984
|
+
id: newId("ccps"),
|
|
985
|
+
contextId: context.id,
|
|
986
|
+
sourceId: context.publishedSourceId,
|
|
987
|
+
membershipId: membership.id,
|
|
988
|
+
itemId: published.itemId,
|
|
989
|
+
itemVersionId: published.itemVersionId,
|
|
990
|
+
submissionId: submission.id,
|
|
991
|
+
createdAt: timestamp,
|
|
992
|
+
ownerEmail: context.ownerEmail,
|
|
993
|
+
orgId: context.orgId ?? null,
|
|
994
|
+
},
|
|
995
|
+
...publishedChildren.map((child) => ({
|
|
996
|
+
id: newId("ccps"),
|
|
997
|
+
contextId: context.id,
|
|
998
|
+
sourceId: context.publishedSourceId,
|
|
999
|
+
membershipId: membership.id,
|
|
1000
|
+
itemId: child.itemId,
|
|
1001
|
+
itemVersionId: child.itemVersionId,
|
|
1002
|
+
submissionId: submission.id,
|
|
1003
|
+
createdAt: timestamp,
|
|
1004
|
+
ownerEmail: context.ownerEmail,
|
|
1005
|
+
orgId: context.orgId ?? null,
|
|
1006
|
+
})),
|
|
1007
|
+
]);
|
|
1008
|
+
await appendAudit(tx, context.id, "approve-submission", {
|
|
1009
|
+
submissionId: submission.id,
|
|
1010
|
+
membershipId: membership.id,
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
export async function submitLatestContextMembershipUpdate(input) {
|
|
1015
|
+
await assertContextRole(input.contextId, "editor");
|
|
1016
|
+
const configured = getCreativeContext();
|
|
1017
|
+
const { getDb, schema } = configured;
|
|
1018
|
+
const [published] = await getDb()
|
|
1019
|
+
.select({
|
|
1020
|
+
membership: schema.creativeContextMemberships,
|
|
1021
|
+
sourceModifiedAt: schema.contextItemVersions.sourceModifiedAt,
|
|
1022
|
+
})
|
|
1023
|
+
.from(schema.creativeContextMemberships)
|
|
1024
|
+
.innerJoin(schema.contextItemVersions, eq(schema.contextItemVersions.id, schema.creativeContextMemberships.publishedItemVersionId))
|
|
1025
|
+
.where(and(eq(schema.creativeContextMemberships.id, input.membershipId), eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.status, "active")))
|
|
1026
|
+
.limit(1);
|
|
1027
|
+
if (!published)
|
|
1028
|
+
throw new Error("Published context membership not found");
|
|
1029
|
+
const reference = parseNativeCreativeArtifactKey(published.membership.artifactKey);
|
|
1030
|
+
if (!reference || reference.appId !== configured.connectorContext.appId) {
|
|
1031
|
+
throw new Error("This context membership is not backed by a native resource in the active app");
|
|
1032
|
+
}
|
|
1033
|
+
const statuses = await resolveNativeCreativeResourceUpdateStatuses([
|
|
1034
|
+
{
|
|
1035
|
+
key: published.membership.id,
|
|
1036
|
+
...reference,
|
|
1037
|
+
publishedSourceModifiedAt: published.sourceModifiedAt,
|
|
1038
|
+
},
|
|
1039
|
+
]);
|
|
1040
|
+
const status = statuses.get(published.membership.id);
|
|
1041
|
+
if (!status)
|
|
1042
|
+
throw new Error("Native resource not found or update status is unavailable");
|
|
1043
|
+
if (status.state !== "update-available")
|
|
1044
|
+
throw new Error("This context membership is already up to date");
|
|
1045
|
+
return manageContextMembership({
|
|
1046
|
+
operation: "submit",
|
|
1047
|
+
contextId: input.contextId,
|
|
1048
|
+
nativeResource: status.reference,
|
|
1049
|
+
note: input.note,
|
|
1050
|
+
rank: published.membership.rank,
|
|
1051
|
+
purpose: published.membership.purpose ?? undefined,
|
|
1052
|
+
confirmBroaderPublication: input.confirmBroaderPublication,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
export async function manageContextMembership(input) {
|
|
1056
|
+
const { getDb, schema } = getCreativeContext();
|
|
1057
|
+
const access = await assertContextRole(input.contextId, input.operation === "submit" ? "editor" : "viewer");
|
|
1058
|
+
const [context] = await getDb()
|
|
1059
|
+
.select()
|
|
1060
|
+
.from(schema.creativeContexts)
|
|
1061
|
+
.where(eq(schema.creativeContexts.id, access.resource.id))
|
|
1062
|
+
.limit(1);
|
|
1063
|
+
if (!context)
|
|
1064
|
+
throw new Error("Creative Context no longer exists");
|
|
1065
|
+
const actor = requireActor();
|
|
1066
|
+
if (input.operation === "submit") {
|
|
1067
|
+
const captured = await resolveSubmissionItem(input);
|
|
1068
|
+
if (!captured.sourceAccess ||
|
|
1069
|
+
visibilityRank(context.visibility) >
|
|
1070
|
+
visibilityRank(captured.sourceAccess.visibility)) {
|
|
1071
|
+
if (!captured.sourceAccess?.canManage) {
|
|
1072
|
+
throw new Error("Only a source manager may publish this artifact into a broader Creative Context");
|
|
1073
|
+
}
|
|
1074
|
+
if (!input.confirmBroaderPublication) {
|
|
1075
|
+
throw new Error("Confirm broader publication before submitting an artifact into a broader Creative Context");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const root = captured.items[0];
|
|
1079
|
+
if (!root)
|
|
1080
|
+
throw new Error("Submission did not include a root artifact");
|
|
1081
|
+
const timestamp = nowIso();
|
|
1082
|
+
let membership = (await getDb()
|
|
1083
|
+
.select()
|
|
1084
|
+
.from(schema.creativeContextMemberships)
|
|
1085
|
+
.where(and(eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.artifactKey, captured.artifactKey)))
|
|
1086
|
+
.limit(1))[0];
|
|
1087
|
+
const membershipId = membership?.id ?? newId("ccmbr");
|
|
1088
|
+
const staged = await writeSnapshot({
|
|
1089
|
+
sourceId: context.stagingSourceId,
|
|
1090
|
+
artifactKey: captured.artifactKey,
|
|
1091
|
+
item: root,
|
|
1092
|
+
ownerEmail: context.ownerEmail,
|
|
1093
|
+
orgId: context.orgId ?? null,
|
|
1094
|
+
});
|
|
1095
|
+
const stagedChildren = await Promise.all(captured.items.slice(1).map(async (item) => {
|
|
1096
|
+
const snapshot = await writeSnapshot({
|
|
1097
|
+
sourceId: context.stagingSourceId,
|
|
1098
|
+
artifactKey: item.externalId,
|
|
1099
|
+
item,
|
|
1100
|
+
ownerEmail: context.ownerEmail,
|
|
1101
|
+
orgId: context.orgId ?? null,
|
|
1102
|
+
});
|
|
1103
|
+
return {
|
|
1104
|
+
artifactKey: item.externalId,
|
|
1105
|
+
...snapshot,
|
|
1106
|
+
};
|
|
1107
|
+
}));
|
|
1108
|
+
for (const child of stagedChildren) {
|
|
1109
|
+
await getDb()
|
|
1110
|
+
.update(schema.contextEdges)
|
|
1111
|
+
.set({ toItemId: child.itemId, toItemVersionId: child.itemVersionId })
|
|
1112
|
+
.where(and(eq(schema.contextEdges.fromItemVersionId, staged.itemVersionId), eq(schema.contextEdges.toExternalId, child.artifactKey)));
|
|
1113
|
+
}
|
|
1114
|
+
const submissionId = newId("ccsub");
|
|
1115
|
+
const autoApprove = context.approvalPolicy === "open" ||
|
|
1116
|
+
(context.approvalPolicy === "admins-only" &&
|
|
1117
|
+
(await currentRequestUserIsOrgAdmin(context.orgId ?? undefined)));
|
|
1118
|
+
await getDb().transaction(async (tx) => {
|
|
1119
|
+
if (membership?.pendingSubmissionId)
|
|
1120
|
+
await tx
|
|
1121
|
+
.update(schema.creativeContextSubmissions)
|
|
1122
|
+
.set({
|
|
1123
|
+
status: "superseded",
|
|
1124
|
+
reviewedBy: actor.ownerEmail,
|
|
1125
|
+
reviewedAt: timestamp,
|
|
1126
|
+
})
|
|
1127
|
+
.where(eq(schema.creativeContextSubmissions.id, membership.pendingSubmissionId));
|
|
1128
|
+
if (!membership)
|
|
1129
|
+
await tx.insert(schema.creativeContextMemberships).values({
|
|
1130
|
+
id: membershipId,
|
|
1131
|
+
contextId: input.contextId,
|
|
1132
|
+
artifactKey: captured.artifactKey,
|
|
1133
|
+
publishedItemId: null,
|
|
1134
|
+
publishedItemVersionId: null,
|
|
1135
|
+
pendingSubmissionId: submissionId,
|
|
1136
|
+
rank: input.rank ?? "normal",
|
|
1137
|
+
purpose: input.purpose ?? null,
|
|
1138
|
+
status: "active",
|
|
1139
|
+
createdAt: timestamp,
|
|
1140
|
+
updatedAt: timestamp,
|
|
1141
|
+
ownerEmail: context.ownerEmail,
|
|
1142
|
+
orgId: context.orgId ?? null,
|
|
1143
|
+
});
|
|
1144
|
+
else
|
|
1145
|
+
await tx
|
|
1146
|
+
.update(schema.creativeContextMemberships)
|
|
1147
|
+
.set({
|
|
1148
|
+
pendingSubmissionId: submissionId,
|
|
1149
|
+
rank: input.rank ?? membership.rank,
|
|
1150
|
+
purpose: input.purpose ?? membership.purpose,
|
|
1151
|
+
status: "active",
|
|
1152
|
+
updatedAt: timestamp,
|
|
1153
|
+
})
|
|
1154
|
+
.where(eq(schema.creativeContextMemberships.id, membershipId));
|
|
1155
|
+
await tx.insert(schema.creativeContextSubmissions).values({
|
|
1156
|
+
id: submissionId,
|
|
1157
|
+
contextId: input.contextId,
|
|
1158
|
+
membershipId,
|
|
1159
|
+
artifactKey: captured.artifactKey,
|
|
1160
|
+
stagingItemId: staged.itemId,
|
|
1161
|
+
stagingItemVersionId: staged.itemVersionId,
|
|
1162
|
+
publishedItemId: null,
|
|
1163
|
+
publishedItemVersionId: null,
|
|
1164
|
+
note: input.note ?? null,
|
|
1165
|
+
privateMetadata: stringifyJson({
|
|
1166
|
+
...captured.privateMetadata,
|
|
1167
|
+
stagedChildren,
|
|
1168
|
+
}),
|
|
1169
|
+
status: "pending",
|
|
1170
|
+
submittedBy: actor.ownerEmail,
|
|
1171
|
+
reviewedBy: null,
|
|
1172
|
+
reviewNote: null,
|
|
1173
|
+
createdAt: timestamp,
|
|
1174
|
+
reviewedAt: null,
|
|
1175
|
+
ownerEmail: context.ownerEmail,
|
|
1176
|
+
orgId: context.orgId ?? null,
|
|
1177
|
+
});
|
|
1178
|
+
await appendAudit(tx, input.contextId, "submit", {
|
|
1179
|
+
membershipId,
|
|
1180
|
+
submissionId,
|
|
1181
|
+
childCount: stagedChildren.length,
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
membership = { id: membershipId };
|
|
1185
|
+
const submission = (await getDb()
|
|
1186
|
+
.select()
|
|
1187
|
+
.from(schema.creativeContextSubmissions)
|
|
1188
|
+
.where(eq(schema.creativeContextSubmissions.id, submissionId))
|
|
1189
|
+
.limit(1))[0];
|
|
1190
|
+
if (autoApprove)
|
|
1191
|
+
await approveSubmission(context, membership, submission);
|
|
1192
|
+
return {
|
|
1193
|
+
membershipId,
|
|
1194
|
+
submission: mapSubmission((await getDb()
|
|
1195
|
+
.select()
|
|
1196
|
+
.from(schema.creativeContextSubmissions)
|
|
1197
|
+
.where(eq(schema.creativeContextSubmissions.id, submissionId))
|
|
1198
|
+
.limit(1))[0]),
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
if (!input.membershipId)
|
|
1202
|
+
throw new Error("membershipId is required");
|
|
1203
|
+
const membership = (await getDb()
|
|
1204
|
+
.select()
|
|
1205
|
+
.from(schema.creativeContextMemberships)
|
|
1206
|
+
.where(and(eq(schema.creativeContextMemberships.id, input.membershipId), eq(schema.creativeContextMemberships.contextId, input.contextId)))
|
|
1207
|
+
.limit(1))[0];
|
|
1208
|
+
if (!membership)
|
|
1209
|
+
throw new Error("Context membership not found");
|
|
1210
|
+
if (input.operation === "remove") {
|
|
1211
|
+
await requireReviewer(input.contextId);
|
|
1212
|
+
const timestamp = nowIso();
|
|
1213
|
+
await getDb().transaction(async (tx) => {
|
|
1214
|
+
await tx
|
|
1215
|
+
.update(schema.creativeContextMemberships)
|
|
1216
|
+
.set({
|
|
1217
|
+
status: "removed",
|
|
1218
|
+
pendingSubmissionId: null,
|
|
1219
|
+
updatedAt: timestamp,
|
|
1220
|
+
})
|
|
1221
|
+
.where(eq(schema.creativeContextMemberships.id, membership.id));
|
|
1222
|
+
const snapshots = await tx
|
|
1223
|
+
.select({
|
|
1224
|
+
itemId: schema.creativeContextPublishedSnapshots.itemId,
|
|
1225
|
+
itemVersionId: schema.creativeContextPublishedSnapshots.itemVersionId,
|
|
1226
|
+
})
|
|
1227
|
+
.from(schema.creativeContextPublishedSnapshots)
|
|
1228
|
+
.where(eq(schema.creativeContextPublishedSnapshots.membershipId, membership.id));
|
|
1229
|
+
const dependencies = snapshots.filter((snapshot) => snapshot.itemId !== membership.publishedItemId ||
|
|
1230
|
+
snapshot.itemVersionId !== membership.publishedItemVersionId);
|
|
1231
|
+
if (!dependencies.length)
|
|
1232
|
+
return;
|
|
1233
|
+
const dependentMemberships = await tx
|
|
1234
|
+
.select({ id: schema.creativeContextMemberships.id })
|
|
1235
|
+
.from(schema.creativeContextMemberships)
|
|
1236
|
+
.where(and(eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.status, "active"), eq(schema.creativeContextMemberships.purpose, "Native artifact child"), or(...dependencies.map((dependency) => and(eq(schema.creativeContextMemberships.publishedItemId, dependency.itemId), eq(schema.creativeContextMemberships.publishedItemVersionId, dependency.itemVersionId))))));
|
|
1237
|
+
const dependentIds = dependentMemberships.map((row) => row.id);
|
|
1238
|
+
if (dependentIds.length) {
|
|
1239
|
+
await tx
|
|
1240
|
+
.update(schema.creativeContextMemberships)
|
|
1241
|
+
.set({
|
|
1242
|
+
status: "removed",
|
|
1243
|
+
pendingSubmissionId: null,
|
|
1244
|
+
updatedAt: timestamp,
|
|
1245
|
+
})
|
|
1246
|
+
.where(inArray(schema.creativeContextMemberships.id, dependentIds));
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
return {
|
|
1250
|
+
membership: mapMembership({
|
|
1251
|
+
...membership,
|
|
1252
|
+
status: "removed",
|
|
1253
|
+
pendingSubmissionId: null,
|
|
1254
|
+
}),
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
const submission = membership.pendingSubmissionId
|
|
1258
|
+
? (await getDb()
|
|
1259
|
+
.select()
|
|
1260
|
+
.from(schema.creativeContextSubmissions)
|
|
1261
|
+
.where(eq(schema.creativeContextSubmissions.id, membership.pendingSubmissionId))
|
|
1262
|
+
.limit(1))[0]
|
|
1263
|
+
: null;
|
|
1264
|
+
if (!submission)
|
|
1265
|
+
throw new Error("No pending submission for this membership");
|
|
1266
|
+
if (input.operation === "withdraw") {
|
|
1267
|
+
if (submission.submittedBy !== actor.ownerEmail)
|
|
1268
|
+
await requireReviewer(input.contextId);
|
|
1269
|
+
await getDb().transaction(async (tx) => {
|
|
1270
|
+
await tx
|
|
1271
|
+
.update(schema.creativeContextSubmissions)
|
|
1272
|
+
.set({
|
|
1273
|
+
status: "withdrawn",
|
|
1274
|
+
reviewedBy: actor.ownerEmail,
|
|
1275
|
+
reviewedAt: nowIso(),
|
|
1276
|
+
})
|
|
1277
|
+
.where(eq(schema.creativeContextSubmissions.id, submission.id));
|
|
1278
|
+
await tx
|
|
1279
|
+
.update(schema.creativeContextMemberships)
|
|
1280
|
+
.set({ pendingSubmissionId: null, updatedAt: nowIso() })
|
|
1281
|
+
.where(eq(schema.creativeContextMemberships.id, membership.id));
|
|
1282
|
+
});
|
|
1283
|
+
return { withdrawn: true };
|
|
1284
|
+
}
|
|
1285
|
+
await requireReviewer(input.contextId);
|
|
1286
|
+
if (input.operation === "request-changes") {
|
|
1287
|
+
await getDb().transaction(async (tx) => {
|
|
1288
|
+
await tx
|
|
1289
|
+
.update(schema.creativeContextSubmissions)
|
|
1290
|
+
.set({
|
|
1291
|
+
status: "rejected",
|
|
1292
|
+
reviewedBy: actor.ownerEmail,
|
|
1293
|
+
reviewNote: input.note ?? "Changes requested",
|
|
1294
|
+
reviewedAt: nowIso(),
|
|
1295
|
+
})
|
|
1296
|
+
.where(eq(schema.creativeContextSubmissions.id, submission.id));
|
|
1297
|
+
await tx
|
|
1298
|
+
.update(schema.creativeContextMemberships)
|
|
1299
|
+
.set({ pendingSubmissionId: null, updatedAt: nowIso() })
|
|
1300
|
+
.where(eq(schema.creativeContextMemberships.id, membership.id));
|
|
1301
|
+
await appendAudit(tx, input.contextId, "request-changes", {
|
|
1302
|
+
submissionId: submission.id,
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
return { requestChanges: true };
|
|
1306
|
+
}
|
|
1307
|
+
await approveSubmission(context, membership, submission);
|
|
1308
|
+
return { approved: true };
|
|
1309
|
+
}
|
|
1310
|
+
export async function resolveNativeContextCloneReference(input) {
|
|
1311
|
+
await assertContextRole(input.contextId, "viewer");
|
|
1312
|
+
const { getDb, schema } = getCreativeContext();
|
|
1313
|
+
const [membership] = await getDb()
|
|
1314
|
+
.select()
|
|
1315
|
+
.from(schema.creativeContextMemberships)
|
|
1316
|
+
.where(and(eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.artifactKey, input.artifactKey), eq(schema.creativeContextMemberships.status, "active")))
|
|
1317
|
+
.limit(1);
|
|
1318
|
+
if (!membership?.publishedItemId || !membership?.publishedItemVersionId)
|
|
1319
|
+
throw new Error("Creative context artifact is not published");
|
|
1320
|
+
const [submission] = await getDb()
|
|
1321
|
+
.select()
|
|
1322
|
+
.from(schema.creativeContextSubmissions)
|
|
1323
|
+
.where(and(eq(schema.creativeContextSubmissions.membershipId, membership.id), eq(schema.creativeContextSubmissions.status, "approved"), eq(schema.creativeContextSubmissions.publishedItemVersionId, membership.publishedItemVersionId)))
|
|
1324
|
+
.limit(1);
|
|
1325
|
+
const metadata = parseJson(submission?.privateMetadata, {});
|
|
1326
|
+
const clone = metadata.clone;
|
|
1327
|
+
if (!clone ||
|
|
1328
|
+
!clone.handle ||
|
|
1329
|
+
clone.appId !== input.appId ||
|
|
1330
|
+
clone.resourceType !== input.resourceType ||
|
|
1331
|
+
clone.resourceId !== input.resourceId ||
|
|
1332
|
+
(input.expectedUpdatedAt !== undefined &&
|
|
1333
|
+
clone.updatedAt !== input.expectedUpdatedAt))
|
|
1334
|
+
throw new Error("Native creative resource reference does not match the governed context submission");
|
|
1335
|
+
return {
|
|
1336
|
+
publishedItemId: membership.publishedItemId,
|
|
1337
|
+
publishedItemVersionId: membership.publishedItemVersionId,
|
|
1338
|
+
cloneHandle: clone.handle,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
//# sourceMappingURL=contexts.js.map
|