@phtngyn/cms 0.1.0-beta.1 → 0.1.0-beta.3
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 +212 -12
- package/bin/cms.mjs +2 -0
- package/package.json +30 -15
- package/src/{Configuration-DyKo0sLS.js → Configuration-CDMYfTre.js} +26 -147
- package/src/Configuration-CDMYfTre.js.map +1 -0
- package/src/Configuration.d.ts +24 -8
- package/src/Configuration.js +0 -1
- package/src/Configuration.js.map +1 -1
- package/src/ContentTransfer-C3Av9sWz.js +53 -0
- package/src/ContentTransfer-C3Av9sWz.js.map +1 -0
- package/src/{DocumentEngine-DYObwMDY.js → DocumentEngine-J7g-emdf.js} +75 -27
- package/src/DocumentEngine-J7g-emdf.js.map +1 -0
- package/src/DocumentModel.d.ts +8 -9
- package/src/DocumentModel.js +13 -3
- package/src/DocumentModel.js.map +1 -1
- package/src/Forms-BsJMev5_.d.ts +74 -0
- package/src/Forms-CfcI3tPU.js +79 -0
- package/src/Forms-CfcI3tPU.js.map +1 -0
- package/src/Forms-Dj6NuW66.js +367 -0
- package/src/Forms-Dj6NuW66.js.map +1 -0
- package/src/Forms.d.ts +51 -0
- package/src/Forms.js +13 -0
- package/src/Forms.js.map +1 -0
- package/src/{HttpPaths-BbS21Jy8.js → HttpPaths-c9nLZ9H2.js} +6 -2
- package/src/HttpPaths-c9nLZ9H2.js.map +1 -0
- package/src/I18n.d.ts +20 -0
- package/src/I18n.js +1 -0
- package/src/Module.js +59 -14
- package/src/Module.js.map +1 -1
- package/src/S3MediaObjects-CpPSzJto.js +2777 -0
- package/src/S3MediaObjects-CpPSzJto.js.map +1 -0
- package/src/Seo-402YumpT.d.ts +198 -0
- package/src/cli/content.js +258 -0
- package/src/cli/content.js.map +1 -0
- package/src/cli.js +71 -0
- package/src/cli.js.map +1 -0
- package/src/{connection-CBTY-TzB.js → connection-CBAy2qGy.js} +5 -7
- package/src/connection-CBAy2qGy.js.map +1 -0
- package/src/{installation-DeuBiFRE.js → installation-CNfIriVm.js} +1007 -2246
- package/src/installation-CNfIriVm.js.map +1 -0
- package/src/internal/authoring-catalog.js +10 -4
- package/src/internal/authoring-catalog.js.map +1 -1
- package/src/internal/authoring-compiler.js +0 -1
- package/src/internal/authoring-compiler.js.map +1 -1
- package/src/internal/authoring-metadata.js +4 -4
- package/src/internal/authoring-metadata.js.map +1 -1
- package/src/internal/cms-config.js +0 -1
- package/src/internal/cms-config.js.map +1 -1
- package/src/internal/configuration.js +26 -17
- package/src/internal/configuration.js.map +1 -1
- package/src/internal/document-model.js +69 -21
- package/src/internal/document-model.js.map +1 -1
- package/src/internal/form-capabilities.js +12 -0
- package/src/internal/form-capabilities.js.map +1 -0
- package/src/internal/i18n.js +47 -0
- package/src/internal/i18n.js.map +1 -0
- package/src/internal/studio-app.js +2 -2
- package/src/internal/studio-app.js.map +1 -1
- package/src/{location-DtYv2aes.js → location-BTqdfIzX.js} +2 -3
- package/src/location-BTqdfIzX.js.map +1 -0
- package/src/{metadata-DE1Mc2td.js → metadata-BEZPWh-3.js} +3 -72
- package/src/metadata-BEZPWh-3.js.map +1 -0
- package/src/{models-C3witQBK.js → models-B4IoTmp7.js} +3 -2
- package/src/models-B4IoTmp7.js.map +1 -0
- package/src/{models-BkMgeaOL.js → models-Bah5B3bT.js} +205 -51
- package/src/models-Bah5B3bT.js.map +1 -0
- package/src/runtime/blocks.js +22 -9
- package/src/runtime/blocks.js.map +1 -1
- package/src/runtime/collaboration.js.map +1 -1
- package/src/runtime/delivery/DocumentRenderer.js +7 -4
- package/src/runtime/delivery/DocumentRenderer.js.map +1 -1
- package/src/runtime/delivery/blocks.js +3 -1
- package/src/runtime/delivery/blocks.js.map +1 -1
- package/src/runtime/delivery/context.js +1 -1
- package/src/runtime/delivery/document-page.js +12 -12
- package/src/runtime/delivery/document-page.js.map +1 -1
- package/src/runtime/delivery/document.js +4 -2
- package/src/runtime/delivery/document.js.map +1 -1
- package/src/runtime/delivery/error.js.map +1 -1
- package/src/runtime/delivery/plugin.js +28 -2
- package/src/runtime/delivery/plugin.js.map +1 -1
- package/src/runtime/delivery/types.d.ts +5 -0
- package/src/runtime/delivery/useCmsDocumentSeo.d.ts +59 -0
- package/src/runtime/delivery/useCmsDocumentSeo.js +101 -0
- package/src/runtime/delivery/useCmsDocumentSeo.js.map +1 -0
- package/src/runtime/delivery/useDocument.js +2 -2
- package/src/runtime/delivery/useDocument.js.map +1 -1
- package/src/runtime/delivery/useDocumentQuery.js +2 -2
- package/src/runtime/delivery/useDocumentQuery.js.map +1 -1
- package/src/runtime/forms/document.js +38 -0
- package/src/runtime/forms/document.js.map +1 -0
- package/src/runtime/forms/useCmsForm.js +190 -0
- package/src/runtime/forms/useCmsForm.js.map +1 -0
- package/src/runtime/i18n/translator.js +51 -0
- package/src/runtime/i18n/translator.js.map +1 -0
- package/src/runtime/i18n/useCmsI18n.d.ts +11 -0
- package/src/runtime/i18n/useCmsI18n.js +37 -0
- package/src/runtime/i18n/useCmsI18n.js.map +1 -0
- package/src/runtime/installation.js +1 -1
- package/src/runtime/plugin.js +3 -3
- package/src/runtime/plugin.js.map +1 -1
- package/src/runtime/preview/comark-document.js +0 -1
- package/src/runtime/preview/comark-document.js.map +1 -1
- package/src/runtime/preview/connection.js +1 -1
- package/src/runtime/preview/controller.js +10 -3
- package/src/runtime/preview/controller.js.map +1 -1
- package/src/runtime/preview/overlay.js.map +1 -1
- package/src/runtime/preview/plugin.client.js +59 -48
- package/src/runtime/preview/plugin.client.js.map +1 -1
- package/src/runtime/preview/protocol.js.map +1 -1
- package/src/runtime/preview/store.js +1 -1
- package/src/runtime/preview/targets.js +0 -1
- package/src/runtime/preview/targets.js.map +1 -1
- package/src/runtime/sitemap-entries.js +17 -0
- package/src/runtime/sitemap-entries.js.map +1 -0
- package/src/runtime/sitemap.js +24 -0
- package/src/runtime/sitemap.js.map +1 -0
- package/src/runtime/studio-app.js.map +1 -1
- package/src/runtime/studio-entry.js +2 -2
- package/src/runtime/studio-entry.js.map +1 -1
- package/src/runtime/studio-navigation.js +1 -1
- package/src/scalars-B2lgGUnj.js +26 -0
- package/src/scalars-B2lgGUnj.js.map +1 -0
- package/src/{store-CfFFx9Ua.js → store-b2CSXgMt.js} +12 -7
- package/src/store-b2CSXgMt.js.map +1 -0
- package/studio/{CreateDocumentDialog--L9lTfx6.js → CreateDocumentDialog-GGey1uBc.js} +44 -44
- package/studio/{collaboration-DUuu678R.js → collaboration-DcK2F6v9.js} +7 -6
- package/studio/{documentPlanning-BXX04Woe.js → documentPlanning-DRErYmBU.js} +7340 -6469
- package/studio/{editor-PexnXkJY.js → editor-Bzlh3zXt.js} +7965 -7955
- package/studio/host.css +1 -1
- package/studio/host.js +10416 -7981
- package/src/Configuration-DyKo0sLS.js.map +0 -1
- package/src/DocumentEngine-DYObwMDY.js.map +0 -1
- package/src/HttpPaths-BbS21Jy8.js.map +0 -1
- package/src/StudioHttp-jR2-bc01.js +0 -718
- package/src/StudioHttp-jR2-bc01.js.map +0 -1
- package/src/connection-CBTY-TzB.js.map +0 -1
- package/src/installation-DeuBiFRE.js.map +0 -1
- package/src/location-DtYv2aes.js.map +0 -1
- package/src/metadata-DE1Mc2td.js.map +0 -1
- package/src/models-BkMgeaOL.js.map +0 -1
- package/src/models-C3witQBK.js.map +0 -1
- package/src/store-CfFFx9Ua.js.map +0 -1
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { a as EditorNodeId, c as RevisionId, d as SourcePath, i as DocumentVersion, r as DocumentId, t as DocumentGeneration } from "./scalars-B2lgGUnj.js";
|
|
2
|
+
import { i as FormResult, n as FORM_SUBMISSION_MAX_BYTES } from "./Forms-CfcI3tPU.js";
|
|
3
|
+
import { a as encodeCmsEditorComponentAttributes, d as createCmsComarkRenderData, i as validateFormValues, t as documentForms } from "./Forms-Dj6NuW66.js";
|
|
4
|
+
import { $ as StudioActor, A as UnpublishDocumentInput, B as SeoMediaUnavailable, D as RemoveDocumentInput, Et as RequestId, H as SeoRevision, J as AccessDenied, K as SeoVersionConflict, O as RestoreDocumentInput, Q as Role, S as PublishDocumentInput, Y as AuthenticationRequired, _t as MediaReference, a as DOCUMENT_REVISION_ACTIONS, at as MediaCleanupReport, b as MarkdownNode, bt as MediaTooLarge, ct as MediaFolder, dt as MediaInvalid, et as StudioEntryDenied, ft as MediaName, ht as MediaPage, it as MediaByteSize, j as UpdateDocumentInput, k as UndeleteDocumentInput, l as DocumentMutationResult, lt as MediaId, m as DocumentSource, mt as MediaObjectKey, n as CmsMarkdownDocument, nt as ImageMimeType, o as DeletedDocument, ot as MediaConflict, p as DocumentRevisionAction, pt as MediaNotFound, rt as MediaAsset, st as MediaCursor, tt as StudioRequestContext, ut as MediaIntegrityReport, vt as MediaReferenced, x as MoveDocumentInput, xt as MediaUpload, y as DuplicateDocumentInput, yt as MediaStatus, z as SeoConfirmationRequired } from "./models-Bah5B3bT.js";
|
|
5
|
+
import { $ as DocumentRevision, A as StudioBadRequest, B as User, C as DocumentStore, D as CurrentStudioRequest, E as defaultAccessControlLayer, F as studioInternalError, G as UserSelfRemovalDenied, H as UserAlreadyExists, I as InitializationAlreadyCompleted, J as CollaborationServerMessage, K as UserSelfRoleChangeDenied, L as InitializationProofInvalid, M as StudioRequestConflict, N as StudioSchemaError, O as StudioApi, P as StudioInternalError, Q as DocumentHistoryInvalidCursor, R as InitializationState, S as DocumentPersistenceError, T as AccessControl, U as UserNotFound, V as UserAdministrationDenied, W as UserPage, X as StateVector, Y as CollaborationTicket, Z as DocumentHistoryComparisonTooLarge, _ as Documents, a as mediaObjectPrefix, at as ActivityEntry, b as cursorCodec, c as Seo, ct as ActivityPage, d as seoDeliveryLayer, dt as DocumentMediaUnavailable, et as ACTIVITY_ACTIONS, f as seoLayer, ft as DocumentNotFound, g as DocumentPreviews, gt as PublishedDocumentNotFound, h as DocumentMaintenance, ht as DocumentVersionConflict, i as MediaObjectStore, it as ActivityCursorInvalid, k as StudioAuthentication, l as SeoDelivery, lt as ActivityTarget, m as SeoStore, mt as DocumentValidationFailed, n as s3MediaObjectStoreLayer, nt as ActivityContext, o as PreviewSeo, ot as ActivityEntryDraft, p as SeoPersistenceError, pt as DocumentPathConflict, q as CollaborationClientMessage, r as MediaPersistenceError, rt as ActivityCursor, s as PublishedSeo, st as ActivityId, tt as ActivityAction, u as makeDefaultSeoOptions, ut as ActivityTargetKind, v as PublishedDocuments, w as renderEditorMarkdown, x as DocumentGenerationConflict, y as documentsLayer, z as CreateUserInput } from "./S3MediaObjects-CpPSzJto.js";
|
|
6
|
+
import { a as CMS_FORM_SUBMIT_PATH, l as CMS_READINESS_PATH, o as CMS_LIVENESS_PATH, p as STUDIO_COLLABORATION_PATH, t as CMS_AUTHENTICATION_PATH, u as STUDIO_API_PATH } from "./HttpPaths-c9nLZ9H2.js";
|
|
7
|
+
import { a as createEditorNodeId, o as editorNodeId, r as identifiedAttributes, t as assertCanonicalEditorContent } from "./metadata-BEZPWh-3.js";
|
|
8
|
+
import { C as finiteNumberAttribute, S as compactJsonAttributes, _ as comarkTagForEditorMark, b as encodeEditorImageAttributes, g as EDITOR_MARK_TYPES, h as comarkToEditor, u as EditorDocumentInvalid, v as isEditorMark, w as nonEmptyStringAttribute, x as normalizeEditorImageLayout, y as EDITOR_IMAGE_LAYOUT_ATTRIBUTE } from "./Configuration-CDMYfTre.js";
|
|
9
|
+
import { o as ContentTransferConflict } from "./ContentTransfer-C3Av9sWz.js";
|
|
10
|
+
import { Cause, Clock, Context, Crypto, Data, DateTime, Deferred, Effect, Encoding, Exit, Fiber, Layer, Option, Predicate, PubSub, Pull, Queue, Redacted, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream, flow } from "effect";
|
|
8
11
|
import { Extension, Node, getSchema, mergeAttributes } from "@tiptap/core";
|
|
9
12
|
import { prosemirrorJSONToYDoc, ySyncPluginKey, yXmlFragmentToProseMirrorRootNode } from "@tiptap/y-tiptap";
|
|
10
|
-
import {
|
|
11
|
-
import { HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse, Multipart } from "effect/unstable/http";
|
|
13
|
+
import { FindMyWay, HttpEffect, HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse, Multipart } from "effect/unstable/http";
|
|
12
14
|
import { HttpApiBuilder, HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
13
|
-
import * as NodeCrypto
|
|
15
|
+
import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto";
|
|
16
|
+
import { createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
|
|
14
17
|
import { betterAuth } from "better-auth";
|
|
15
18
|
import { createAccessControl } from "better-auth/plugins/access";
|
|
16
19
|
import { admin } from "better-auth/plugins/admin";
|
|
17
20
|
import { defaultStatements } from "better-auth/plugins/admin/access";
|
|
18
|
-
import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
|
|
19
21
|
import { Hocuspocus, IncomingMessage, MessageType, shouldSkipStoreHooks } from "@hocuspocus/server";
|
|
20
|
-
import { renderMarkdown } from "comark/render";
|
|
21
|
-
import { diffArrays } from "diff";
|
|
22
22
|
import StarterKit from "@tiptap/starter-kit";
|
|
23
23
|
import * as Y from "yjs";
|
|
24
24
|
import { Doc, applyUpdate, encodeStateAsUpdate, encodeStateVector } from "yjs";
|
|
25
|
-
import {
|
|
25
|
+
import { RateLimiter } from "effect/unstable/persistence";
|
|
26
26
|
import { PgClient } from "@effect/sql-pg";
|
|
27
27
|
import { isDeepStrictEqual } from "node:util";
|
|
28
|
-
import { and, asc, count, desc, eq, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
28
|
+
import { and, asc, count, desc, eq, gt, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
29
29
|
import * as PgDrizzle from "drizzle-orm/effect-postgres";
|
|
30
30
|
import { createInsertSchema, createSelectSchema } from "drizzle-orm/effect-schema";
|
|
31
31
|
import { alias, bigint, boolean, bytea, check, foreignKey, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
|
@@ -58,50 +58,6 @@ function activityEntryFromDraft(id, draft, occurredAt) {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//#endregion
|
|
62
|
-
//#region ../server/src/internal/access-control.ts
|
|
63
|
-
const VIEWER_ACTIONS = /* @__PURE__ */ new Set([
|
|
64
|
-
"document.browse",
|
|
65
|
-
"document.read",
|
|
66
|
-
"document.trash",
|
|
67
|
-
"document.history",
|
|
68
|
-
"media.read",
|
|
69
|
-
"media.search",
|
|
70
|
-
"media.integrity"
|
|
71
|
-
]);
|
|
72
|
-
const EDITOR_ACTIONS = /* @__PURE__ */ new Set([...DOCUMENT_ACTIONS, ...MEDIA_ACTIONS.filter((action) => action !== "media.cleanup")]);
|
|
73
|
-
var AccessControl = class extends Context.Service()("@internal/server/AccessControl") {};
|
|
74
|
-
const defaultAccessControlLayer = Layer.succeed(AccessControl, AccessControl.of({ require: Effect.fn("AccessControl.require")(function* (context, decision) {
|
|
75
|
-
if (isAllowed(context, decision.action)) return;
|
|
76
|
-
return yield* new AccessDenied({
|
|
77
|
-
action: decision.action,
|
|
78
|
-
actorId: context.actor.id,
|
|
79
|
-
resource: decision.resource
|
|
80
|
-
});
|
|
81
|
-
}) }));
|
|
82
|
-
function isAllowed(context, action) {
|
|
83
|
-
return context.actor.roles.some((role) => {
|
|
84
|
-
if (role === "administrator") return true;
|
|
85
|
-
return role === "editor" && EDITOR_ACTIONS.has(action) || role === "viewer" && VIEWER_ACTIONS.has(action);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
//#endregion
|
|
90
|
-
//#region ../server/src/internal/cursor.ts
|
|
91
|
-
/** Owns the opaque base64url JSON representation shared by keyset cursors. */
|
|
92
|
-
function cursorCodec(schema, makeCursor, invalid) {
|
|
93
|
-
const decodeJson = Schema.decodeUnknownResult(schema);
|
|
94
|
-
const encodeJson = Schema.encodeSync(schema);
|
|
95
|
-
return {
|
|
96
|
-
decode: (cursor) => {
|
|
97
|
-
const decoded = Encoding.decodeBase64UrlString(cursor);
|
|
98
|
-
if (Result.isFailure(decoded)) return Result.fail(invalid());
|
|
99
|
-
return decodeJson(decoded.success).pipe(Result.mapError(invalid));
|
|
100
|
-
},
|
|
101
|
-
encode: (payload) => makeCursor(Encoding.encodeBase64Url(encodeJson(payload)))
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
61
|
//#endregion
|
|
106
62
|
//#region ../server/src/internal/activity/activity.ts
|
|
107
63
|
var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailable", {
|
|
@@ -109,11 +65,11 @@ var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailab
|
|
|
109
65
|
cause: Schema.Defect()
|
|
110
66
|
}) {};
|
|
111
67
|
var Activity = class extends Context.Service()("@internal/server/Activity") {};
|
|
112
|
-
const CursorPayload$
|
|
68
|
+
const CursorPayload$1 = Schema.Struct({
|
|
113
69
|
id: ActivityId,
|
|
114
70
|
occurredAt: Schema.DateTimeUtcFromString
|
|
115
71
|
});
|
|
116
|
-
const cursors$
|
|
72
|
+
const cursors$1 = cursorCodec(Schema.fromJsonString(CursorPayload$1), (value) => ActivityCursor.make(value), () => new ActivityCursorInvalid());
|
|
117
73
|
const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
|
|
118
74
|
const accessControl = yield* AccessControl;
|
|
119
75
|
const storage = yield* ActivityStore;
|
|
@@ -122,7 +78,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
|
|
|
122
78
|
action: "activity.read",
|
|
123
79
|
resource: { kind: "activity" }
|
|
124
80
|
});
|
|
125
|
-
const cursor = query.cursor ? yield* Effect.fromResult(cursors$
|
|
81
|
+
const cursor = query.cursor ? yield* Effect.fromResult(cursors$1.decode(query.cursor)) : void 0;
|
|
126
82
|
const page = yield* storage.list({
|
|
127
83
|
action: query.action,
|
|
128
84
|
actorId: query.actorId,
|
|
@@ -138,7 +94,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
|
|
|
138
94
|
};
|
|
139
95
|
return page.next ? ActivityPage.make({
|
|
140
96
|
...output,
|
|
141
|
-
nextCursor: cursors$
|
|
97
|
+
nextCursor: cursors$1.encode(page.next)
|
|
142
98
|
}) : ActivityPage.make(output);
|
|
143
99
|
});
|
|
144
100
|
return Activity.of({ list });
|
|
@@ -254,7 +210,6 @@ const ROUND = [
|
|
|
254
210
|
3204031479,
|
|
255
211
|
3329325298
|
|
256
212
|
];
|
|
257
|
-
/** Portable synchronous SHA-256 for deterministic journal fingerprints and secret redaction. */
|
|
258
213
|
function sha256Hex(value) {
|
|
259
214
|
const input = new TextEncoder().encode(value);
|
|
260
215
|
const bitLength = BigInt(input.length) * 8n;
|
|
@@ -334,14 +289,14 @@ const PendingChange = ChangeDraft.pipe(Schema.fieldsAssign({
|
|
|
334
289
|
updatedAt: Schema.DateTimeUtcFromString
|
|
335
290
|
}));
|
|
336
291
|
function changeFingerprint(change) {
|
|
337
|
-
const encoded = Schema.
|
|
292
|
+
const encoded = Result.getOrThrow(Schema.encodeResult(ChangeDraft)(change));
|
|
338
293
|
return sha256Hex(JSON.stringify(canonicalJson(encoded)));
|
|
339
294
|
}
|
|
340
|
-
const isJsonObject$
|
|
341
|
-
const decodeJson$2 = Schema.
|
|
295
|
+
const isJsonObject$4 = Schema.is(Schema.JsonObject);
|
|
296
|
+
const decodeJson$2 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
|
|
342
297
|
function canonicalJson(value) {
|
|
343
298
|
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
344
|
-
if (!isJsonObject$
|
|
299
|
+
if (!isJsonObject$4(value)) return value;
|
|
345
300
|
return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, canonicalJson(decodeJson$2(value[key]))]));
|
|
346
301
|
}
|
|
347
302
|
|
|
@@ -349,17 +304,17 @@ function canonicalJson(value) {
|
|
|
349
304
|
//#region ../server/src/internal/change-journal/change-journal.ts
|
|
350
305
|
const ChangeBegin = Data.taggedEnum();
|
|
351
306
|
function requireUnresolvedChange(result) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
changeId:
|
|
307
|
+
return ChangeBegin.$match(result, {
|
|
308
|
+
Unresolved: ({ change }) => Effect.succeed(change),
|
|
309
|
+
Recorded: ({ entry }) => Effect.fail(new ChangeAlreadyResolved({
|
|
310
|
+
changeId: entry.id,
|
|
356
311
|
outcome: "applied"
|
|
357
|
-
}))
|
|
358
|
-
|
|
359
|
-
changeId
|
|
312
|
+
})),
|
|
313
|
+
Cancelled: ({ changeId }) => Effect.fail(new ChangeAlreadyResolved({
|
|
314
|
+
changeId,
|
|
360
315
|
outcome: "cancelled"
|
|
361
|
-
}))
|
|
362
|
-
}
|
|
316
|
+
}))
|
|
317
|
+
});
|
|
363
318
|
}
|
|
364
319
|
var ChangeJournal = class extends Context.Service()("@internal/server/ChangeJournal") {};
|
|
365
320
|
const findPendingChange = Effect.fn("ChangeJournal.findPending")(function* (journal, subject) {
|
|
@@ -424,7 +379,7 @@ function initialAdministratorDraft(identity, intent) {
|
|
|
424
379
|
action: "user.created",
|
|
425
380
|
context: identity.context,
|
|
426
381
|
id: identity.id,
|
|
427
|
-
intent: Schema.
|
|
382
|
+
intent: Result.getOrThrow(Schema.encodeResult(InitialAdministratorIntent)(intent)),
|
|
428
383
|
scope: "initialization",
|
|
429
384
|
subject: userEmailSubject(intent.normalizedEmail)
|
|
430
385
|
});
|
|
@@ -566,7 +521,7 @@ function userChangeDraft(identity, intent) {
|
|
|
566
521
|
action: userChangeAction(intent),
|
|
567
522
|
context: identity.context,
|
|
568
523
|
id: identity.id,
|
|
569
|
-
intent: Schema.
|
|
524
|
+
intent: Result.getOrThrow(Schema.encodeResult(UserChangeIntent)(intent)),
|
|
570
525
|
scope: "users",
|
|
571
526
|
subject: userEmailSubject(intent.kind === "create" ? intent.normalizedEmail : intent.email)
|
|
572
527
|
};
|
|
@@ -732,12 +687,22 @@ function appliedResolution(change, intent, userId) {
|
|
|
732
687
|
//#region ../auth/src/internal/user.ts
|
|
733
688
|
const cmsUserCreationChangeField = "cmsCreationChangeId";
|
|
734
689
|
const cmsUserSessionsRevokedChangeField = "cmsSessionsRevokedChangeId";
|
|
690
|
+
const BetterAuthUser = Schema.Struct({
|
|
691
|
+
banned: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
|
|
692
|
+
createdAt: Schema.Date,
|
|
693
|
+
email: Schema.String,
|
|
694
|
+
emailVerified: Schema.Boolean,
|
|
695
|
+
id: Schema.String,
|
|
696
|
+
name: Schema.String,
|
|
697
|
+
role: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
698
|
+
updatedAt: Schema.Date
|
|
699
|
+
});
|
|
735
700
|
const BetterAuthUserRecoveryEvidence = Schema.Struct({
|
|
736
701
|
[cmsUserCreationChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId))),
|
|
737
702
|
[cmsUserSessionsRevokedChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId)))
|
|
738
703
|
});
|
|
739
|
-
function decodeBetterAuthUser(
|
|
740
|
-
return Schema.decodeUnknownEffect(User)({
|
|
704
|
+
function decodeBetterAuthUser(value) {
|
|
705
|
+
return Schema.decodeEffect(BetterAuthUser)(value).pipe(Effect.flatMap((user) => Schema.decodeUnknownEffect(User)({
|
|
741
706
|
banned: user.banned === true,
|
|
742
707
|
createdAt: user.createdAt.toISOString(),
|
|
743
708
|
email: user.email,
|
|
@@ -746,7 +711,7 @@ function decodeBetterAuthUser(user) {
|
|
|
746
711
|
name: user.name,
|
|
747
712
|
role: user.role,
|
|
748
713
|
updatedAt: user.updatedAt.toISOString()
|
|
749
|
-
});
|
|
714
|
+
})));
|
|
750
715
|
}
|
|
751
716
|
function decodeBetterAuthActor(user) {
|
|
752
717
|
return Schema.decodeEffect(StudioActor)(betterAuthActorInput(user));
|
|
@@ -783,12 +748,12 @@ function betterAuthActivityWriter(storage, crypto) {
|
|
|
783
748
|
return yield* storage.append(activityEntryFromDraft(id, draft, yield* DateTime.now));
|
|
784
749
|
});
|
|
785
750
|
}
|
|
786
|
-
function betterAuthActivityOptions(writeActivity, crypto) {
|
|
751
|
+
function betterAuthActivityOptions(writeActivity, crypto, runPromise) {
|
|
787
752
|
const capturedActors = /* @__PURE__ */ new WeakMap();
|
|
788
|
-
const write = (action, actor) =>
|
|
753
|
+
const write = (action, actor) => runPromise(crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((requestId) => activityDraft(action, actor, requestId)), Effect.flatMap(writeActivity), Effect.asVoid, Effect.catchCauseIf((cause) => Cause.interruptors(cause).size === 0, () => Effect.logWarning("Authentication Activity could not be recorded.", { action }).pipe(Effect.ignoreCause))));
|
|
789
754
|
const before = createAuthMiddleware((context) => {
|
|
790
755
|
if (context.path !== "/sign-out" && context.path !== "/change-password") return Promise.resolve();
|
|
791
|
-
return
|
|
756
|
+
return runPromise(Effect.tryPromise(() => getSessionFromCtx(context, { disableRefresh: true })).pipe(Effect.orElseSucceed(() => null), Effect.tap((session) => Effect.sync(() => {
|
|
792
757
|
const actor = session?.user ? decodeActor(session.user) : void 0;
|
|
793
758
|
if (actor) capturedActors.set(context.context, actor);
|
|
794
759
|
})), Effect.asVoid));
|
|
@@ -832,7 +797,149 @@ function activityDraft(action, actor, requestId) {
|
|
|
832
797
|
}
|
|
833
798
|
function decodeActor(user) {
|
|
834
799
|
const option = decodeBetterAuthActorOption(user);
|
|
835
|
-
return
|
|
800
|
+
return Option.getOrUndefined(option);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region ../auth/src/internal/better-auth.ts
|
|
805
|
+
const cmsBetterAuthAccess = createAccessControl(defaultStatements);
|
|
806
|
+
const cmsBetterAuthRoles = {
|
|
807
|
+
administrator: cmsBetterAuthAccess.newRole({
|
|
808
|
+
session: ["list", "revoke"],
|
|
809
|
+
user: [
|
|
810
|
+
"create",
|
|
811
|
+
"list",
|
|
812
|
+
"get",
|
|
813
|
+
"set-role",
|
|
814
|
+
"delete",
|
|
815
|
+
"update"
|
|
816
|
+
]
|
|
817
|
+
}),
|
|
818
|
+
editor: cmsBetterAuthAccess.newRole({
|
|
819
|
+
session: [],
|
|
820
|
+
user: []
|
|
821
|
+
}),
|
|
822
|
+
viewer: cmsBetterAuthAccess.newRole({
|
|
823
|
+
session: [],
|
|
824
|
+
user: []
|
|
825
|
+
})
|
|
826
|
+
};
|
|
827
|
+
const timestampFields = {
|
|
828
|
+
createdAt: "created_at",
|
|
829
|
+
updatedAt: "updated_at"
|
|
830
|
+
};
|
|
831
|
+
function createCmsBetterAuthOptions(options, activity) {
|
|
832
|
+
return {
|
|
833
|
+
advanced: { database: {
|
|
834
|
+
generateId: "uuid",
|
|
835
|
+
joins: true
|
|
836
|
+
} },
|
|
837
|
+
appName: "CMS Studio",
|
|
838
|
+
basePath: options.basePath ?? "/api/cms/auth",
|
|
839
|
+
baseURL: options.baseURL,
|
|
840
|
+
database: options.database,
|
|
841
|
+
emailAndPassword: {
|
|
842
|
+
disableSignUp: true,
|
|
843
|
+
enabled: true,
|
|
844
|
+
...activity?.emailAndPassword
|
|
845
|
+
},
|
|
846
|
+
hooks: activity?.hooks,
|
|
847
|
+
user: {
|
|
848
|
+
additionalFields: {
|
|
849
|
+
[cmsUserCreationChangeField]: hiddenChangeField("cms_creation_change_id"),
|
|
850
|
+
[cmsUserSessionsRevokedChangeField]: hiddenChangeField("cms_sessions_revoked_change_id")
|
|
851
|
+
},
|
|
852
|
+
modelName: "auth_users",
|
|
853
|
+
fields: {
|
|
854
|
+
...timestampFields,
|
|
855
|
+
emailVerified: "email_verified"
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
session: {
|
|
859
|
+
cookieCache: {
|
|
860
|
+
enabled: false,
|
|
861
|
+
strategy: "jwt"
|
|
862
|
+
},
|
|
863
|
+
modelName: "auth_sessions",
|
|
864
|
+
fields: {
|
|
865
|
+
...timestampFields,
|
|
866
|
+
expiresAt: "expires_at",
|
|
867
|
+
ipAddress: "ip_address",
|
|
868
|
+
userAgent: "user_agent",
|
|
869
|
+
userId: "user_id"
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
account: {
|
|
873
|
+
modelName: "auth_accounts",
|
|
874
|
+
fields: {
|
|
875
|
+
accessToken: "access_token",
|
|
876
|
+
accessTokenExpiresAt: "access_token_expires_at",
|
|
877
|
+
accountId: "account_id",
|
|
878
|
+
...timestampFields,
|
|
879
|
+
idToken: "id_token",
|
|
880
|
+
providerId: "provider_id",
|
|
881
|
+
refreshToken: "refresh_token",
|
|
882
|
+
refreshTokenExpiresAt: "refresh_token_expires_at",
|
|
883
|
+
userId: "user_id"
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
verification: {
|
|
887
|
+
modelName: "auth_verifications",
|
|
888
|
+
fields: {
|
|
889
|
+
...timestampFields,
|
|
890
|
+
expiresAt: "expires_at"
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
rateLimit: {
|
|
894
|
+
enabled: true,
|
|
895
|
+
storage: "database",
|
|
896
|
+
modelName: "auth_rate_limits",
|
|
897
|
+
fields: { lastRequest: "last_request" }
|
|
898
|
+
},
|
|
899
|
+
plugins: [admin({
|
|
900
|
+
ac: cmsBetterAuthAccess,
|
|
901
|
+
adminRoles: ["administrator"],
|
|
902
|
+
defaultRole: "editor",
|
|
903
|
+
roles: cmsBetterAuthRoles,
|
|
904
|
+
schema: {
|
|
905
|
+
session: { fields: { impersonatedBy: "impersonated_by" } },
|
|
906
|
+
user: { fields: {
|
|
907
|
+
banExpires: "ban_expires",
|
|
908
|
+
banReason: "ban_reason",
|
|
909
|
+
banned: "banned",
|
|
910
|
+
role: "role"
|
|
911
|
+
} }
|
|
912
|
+
}
|
|
913
|
+
})],
|
|
914
|
+
secret: options.secret
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function createCmsBetterAuth(options, activity) {
|
|
918
|
+
return betterAuth(createCmsBetterAuthOptions(options, activity));
|
|
919
|
+
}
|
|
920
|
+
function hiddenChangeField(fieldName) {
|
|
921
|
+
return {
|
|
922
|
+
fieldName,
|
|
923
|
+
input: false,
|
|
924
|
+
required: false,
|
|
925
|
+
returned: false,
|
|
926
|
+
type: "string"
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region ../auth/src/internal/handler.ts
|
|
932
|
+
function rejectExternalAdminRoutes(handler, basePath) {
|
|
933
|
+
const mountedAt = canonicalBasePath(basePath);
|
|
934
|
+
return (request) => {
|
|
935
|
+
const path = new URL(request.url).pathname;
|
|
936
|
+
const relative = mountedAt === "/" ? path : path === mountedAt ? "/" : path.startsWith(`${mountedAt}/`) ? path.slice(mountedAt.length) : path;
|
|
937
|
+
if (relative === "/admin" || relative.startsWith("/admin/")) return Promise.resolve(new Response(null, { status: 404 }));
|
|
938
|
+
return handler(request);
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function canonicalBasePath(basePath) {
|
|
942
|
+
return (basePath.startsWith("/") ? basePath : `/${basePath}`).replace(/\/+$/u, "") || "/";
|
|
836
943
|
}
|
|
837
944
|
|
|
838
945
|
//#endregion
|
|
@@ -915,9 +1022,24 @@ function matches(code, ...candidates) {
|
|
|
915
1022
|
return code !== void 0 && candidates.some((candidate) => candidate.code === code);
|
|
916
1023
|
}
|
|
917
1024
|
function apiErrorCode(cause) {
|
|
918
|
-
if (!(cause instanceof APIError)
|
|
919
|
-
const
|
|
920
|
-
|
|
1025
|
+
if (!(cause instanceof Error) || cause.name !== "APIError") return void 0;
|
|
1026
|
+
const apiError = Schema.decodeUnknownOption(Schema.Struct({
|
|
1027
|
+
body: Schema.Struct({ code: Schema.String }),
|
|
1028
|
+
statusCode: Schema.Int
|
|
1029
|
+
}))(cause);
|
|
1030
|
+
return Option.map(apiError, ({ body }) => body.code).pipe(Option.getOrUndefined);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region ../auth/src/internal/provider.ts
|
|
1035
|
+
function providerCall(operation, request, onError) {
|
|
1036
|
+
return Effect.tryPromise({
|
|
1037
|
+
catch: (cause) => onError(operation, cause),
|
|
1038
|
+
try: request
|
|
1039
|
+
}).pipe(Effect.withSpan(operation));
|
|
1040
|
+
}
|
|
1041
|
+
function providerAdapter(auth, operation, onError) {
|
|
1042
|
+
return providerCall(operation, () => auth.$context, onError).pipe(Effect.map((context) => context.internalAdapter));
|
|
921
1043
|
}
|
|
922
1044
|
|
|
923
1045
|
//#endregion
|
|
@@ -926,13 +1048,10 @@ function betterAuthIdentity(auth) {
|
|
|
926
1048
|
const authorityHeaders = /* @__PURE__ */ new WeakMap();
|
|
927
1049
|
const authenticate = Effect.fn("BetterAuth.Authentication.authenticate")(function* (headers) {
|
|
928
1050
|
const providerHeaders = new globalThis.Headers(headers);
|
|
929
|
-
const session = yield*
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
query: { disableCookieCache: true }
|
|
934
|
-
})
|
|
935
|
-
});
|
|
1051
|
+
const session = yield* providerCall("BetterAuth.getSession", () => auth.api.getSession({
|
|
1052
|
+
headers: providerHeaders,
|
|
1053
|
+
query: { disableCookieCache: true }
|
|
1054
|
+
}), () => studioInternalError());
|
|
936
1055
|
if (!session?.user) return null;
|
|
937
1056
|
const actor = yield* decodeBetterAuthActor(session.user).pipe(Effect.mapError(() => studioInternalError()));
|
|
938
1057
|
const authority = issueIdentityAuthority();
|
|
@@ -1026,16 +1145,13 @@ function betterAuthIdentity(auth) {
|
|
|
1026
1145
|
const apply = (changeId) => applyOperation(auth, "BetterAuth.revokeUserSessions", command, () => auth.api.revokeUserSessions({
|
|
1027
1146
|
body: { userId: command.userId },
|
|
1028
1147
|
headers
|
|
1029
|
-
})).pipe(Effect.flatMap(() =>
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
headers
|
|
1037
|
-
})
|
|
1038
|
-
})), Effect.as(user));
|
|
1148
|
+
})).pipe(Effect.flatMap(() => providerCall("BetterAuth.adminUpdateUser.sessionRevocationReceipt", () => auth.api.adminUpdateUser({
|
|
1149
|
+
body: {
|
|
1150
|
+
data: { [cmsUserSessionsRevokedChangeField]: changeId },
|
|
1151
|
+
userId: command.userId
|
|
1152
|
+
},
|
|
1153
|
+
headers
|
|
1154
|
+
}), (operation, cause) => IdentityApplyFailure.Unknown({ error: unavailable$4(operation, cause) }))), Effect.as(user));
|
|
1039
1155
|
return IdentityPreparation.Prepared({
|
|
1040
1156
|
apply,
|
|
1041
1157
|
intent,
|
|
@@ -1053,22 +1169,13 @@ function betterAuthIdentity(auth) {
|
|
|
1053
1169
|
};
|
|
1054
1170
|
}
|
|
1055
1171
|
function prepareOperation(auth, operation, request) {
|
|
1056
|
-
return
|
|
1057
|
-
catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause),
|
|
1058
|
-
try: request
|
|
1059
|
-
});
|
|
1172
|
+
return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause));
|
|
1060
1173
|
}
|
|
1061
1174
|
function prepareUserOperation(auth, operation, userId, request) {
|
|
1062
|
-
return
|
|
1063
|
-
catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause, userId),
|
|
1064
|
-
try: request
|
|
1065
|
-
});
|
|
1175
|
+
return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause, userId));
|
|
1066
1176
|
}
|
|
1067
1177
|
function applyOperation(auth, operation, command, request) {
|
|
1068
|
-
return
|
|
1069
|
-
catch: (cause) => applyError(auth.$ERROR_CODES, operation, command, cause),
|
|
1070
|
-
try: request
|
|
1071
|
-
});
|
|
1178
|
+
return providerCall(operation, request, (name, cause) => applyError(auth.$ERROR_CODES, name, command, cause));
|
|
1072
1179
|
}
|
|
1073
1180
|
function requireAuthority(authorityHeaders, authority) {
|
|
1074
1181
|
const headers = authorityHeaders.get(authority);
|
|
@@ -1106,13 +1213,10 @@ function betterAuthInitialIdentity(auth) {
|
|
|
1106
1213
|
});
|
|
1107
1214
|
const intent = initialAdministratorIntent(input);
|
|
1108
1215
|
return {
|
|
1109
|
-
apply:
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
...createInput
|
|
1114
|
-
} })
|
|
1115
|
-
}).pipe(Effect.flatMap((result) => decodeBetterAuthUser(result.user).pipe(Effect.mapError((cause) => InitialIdentityFailure.Unknown({ error: initializationUnavailable$1("BetterAuth.initialization.createUser", cause) }))))),
|
|
1216
|
+
apply: providerCall("BetterAuth.initialization.createUser", () => auth.api.createUser({ body: {
|
|
1217
|
+
data: { emailVerified: true },
|
|
1218
|
+
...createInput
|
|
1219
|
+
} }), (operation, cause) => initialIdentityError(auth.$ERROR_CODES, operation, createInput, cause)).pipe(Effect.flatMap((result) => decodeBetterAuthUser(result.user).pipe(Effect.mapError((cause) => InitialIdentityFailure.Unknown({ error: initializationUnavailable$1("BetterAuth.initialization.createUser", cause) }))))),
|
|
1116
1220
|
intent
|
|
1117
1221
|
};
|
|
1118
1222
|
} });
|
|
@@ -1121,22 +1225,13 @@ function betterAuthInitialIdentity(auth) {
|
|
|
1121
1225
|
//#endregion
|
|
1122
1226
|
//#region ../auth/src/internal/recovery.ts
|
|
1123
1227
|
function betterAuthRecovery(auth) {
|
|
1124
|
-
const userAdapter =
|
|
1125
|
-
|
|
1126
|
-
try: () => auth.$context
|
|
1127
|
-
}).pipe(Effect.map((context) => context.internalAdapter));
|
|
1128
|
-
const initialAdapter = Effect.tryPromise({
|
|
1129
|
-
catch: (cause) => initializationUnavailable$1("BetterAuth.initialization.recovery.context", cause),
|
|
1130
|
-
try: () => auth.$context
|
|
1131
|
-
}).pipe(Effect.map((context) => context.internalAdapter));
|
|
1228
|
+
const userAdapter = providerAdapter(auth, "BetterAuth.recovery.context", unavailable$4);
|
|
1229
|
+
const initialAdapter = providerAdapter(auth, "BetterAuth.initialization.recovery.context", initializationUnavailable$1);
|
|
1132
1230
|
const users = IdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeUserChange")(function* (intent) {
|
|
1133
1231
|
const adapter = yield* userAdapter;
|
|
1134
1232
|
switch (intent.kind) {
|
|
1135
1233
|
case "create": {
|
|
1136
|
-
const result = yield*
|
|
1137
|
-
catch: (cause) => unavailable$4("BetterAuth.recovery.findUserByEmail", cause),
|
|
1138
|
-
try: () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true })
|
|
1139
|
-
});
|
|
1234
|
+
const result = yield* providerCall("BetterAuth.recovery.findUserByEmail", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), unavailable$4);
|
|
1140
1235
|
if (!result) return {
|
|
1141
1236
|
creationChangeId: null,
|
|
1142
1237
|
credentialReady: false,
|
|
@@ -1178,10 +1273,7 @@ function betterAuthRecovery(auth) {
|
|
|
1178
1273
|
return {
|
|
1179
1274
|
initial: InitialIdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeInitialAdministrator")(function* (intent) {
|
|
1180
1275
|
const adapter = yield* initialAdapter;
|
|
1181
|
-
const result = yield*
|
|
1182
|
-
catch: (cause) => initializationUnavailable$1("BetterAuth.recovery.findInitialAdministrator", cause),
|
|
1183
|
-
try: () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true })
|
|
1184
|
-
});
|
|
1276
|
+
const result = yield* providerCall("BetterAuth.recovery.findInitialAdministrator", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), initializationUnavailable$1);
|
|
1185
1277
|
if (!result) return {
|
|
1186
1278
|
credentialReady: false,
|
|
1187
1279
|
user: null
|
|
@@ -1204,1580 +1296,156 @@ function findUser(adapter, userId) {
|
|
|
1204
1296
|
return findRawUser(adapter, userId).pipe(Effect.flatMap((user) => user ? decodeUser(user, "BetterAuth.recovery.findUserById") : Effect.succeed(null)));
|
|
1205
1297
|
}
|
|
1206
1298
|
function findRawUser(adapter, userId) {
|
|
1207
|
-
return
|
|
1208
|
-
catch: (cause) => unavailable$4("BetterAuth.recovery.findUserById", cause),
|
|
1209
|
-
try: () => adapter.findUserById(userId)
|
|
1210
|
-
});
|
|
1299
|
+
return providerCall("BetterAuth.recovery.findUserById", () => adapter.findUserById(userId), unavailable$4);
|
|
1211
1300
|
}
|
|
1212
1301
|
function findSessionFingerprints(adapter, userId) {
|
|
1213
|
-
return Effect.
|
|
1214
|
-
catch: (cause) => unavailable$4("BetterAuth.recovery.listSessions", cause),
|
|
1215
|
-
try: () => adapter.listSessions(userId)
|
|
1216
|
-
}).pipe(Effect.map((sessions) => sessions.map((session) => sessionFingerprint(session.id))));
|
|
1302
|
+
return providerCall("BetterAuth.recovery.listSessions", () => adapter.listSessions(userId), unavailable$4).pipe(Effect.map((sessions) => sessions.map((session) => sessionFingerprint(session.id))));
|
|
1217
1303
|
}
|
|
1218
1304
|
|
|
1219
1305
|
//#endregion
|
|
1220
|
-
//#region ../auth/src/
|
|
1221
|
-
const cmsBetterAuthAccess = createAccessControl(defaultStatements);
|
|
1222
|
-
const cmsBetterAuthRoles = {
|
|
1223
|
-
administrator: cmsBetterAuthAccess.newRole({
|
|
1224
|
-
session: ["list", "revoke"],
|
|
1225
|
-
user: [
|
|
1226
|
-
"create",
|
|
1227
|
-
"list",
|
|
1228
|
-
"get",
|
|
1229
|
-
"set-role",
|
|
1230
|
-
"delete",
|
|
1231
|
-
"update"
|
|
1232
|
-
]
|
|
1233
|
-
}),
|
|
1234
|
-
editor: cmsBetterAuthAccess.newRole({
|
|
1235
|
-
session: [],
|
|
1236
|
-
user: []
|
|
1237
|
-
}),
|
|
1238
|
-
viewer: cmsBetterAuthAccess.newRole({
|
|
1239
|
-
session: [],
|
|
1240
|
-
user: []
|
|
1241
|
-
})
|
|
1242
|
-
};
|
|
1243
|
-
const timestampFields = {
|
|
1244
|
-
createdAt: "created_at",
|
|
1245
|
-
updatedAt: "updated_at"
|
|
1246
|
-
};
|
|
1247
|
-
function createCmsBetterAuthOptions(options, activity) {
|
|
1248
|
-
return {
|
|
1249
|
-
advanced: { database: {
|
|
1250
|
-
generateId: "uuid",
|
|
1251
|
-
joins: true
|
|
1252
|
-
} },
|
|
1253
|
-
appName: "CMS Studio",
|
|
1254
|
-
basePath: options.basePath ?? "/api/cms/auth",
|
|
1255
|
-
baseURL: options.baseURL,
|
|
1256
|
-
database: options.database,
|
|
1257
|
-
emailAndPassword: {
|
|
1258
|
-
disableSignUp: true,
|
|
1259
|
-
enabled: true,
|
|
1260
|
-
...activity?.emailAndPassword
|
|
1261
|
-
},
|
|
1262
|
-
hooks: activity?.hooks,
|
|
1263
|
-
user: {
|
|
1264
|
-
additionalFields: {
|
|
1265
|
-
[cmsUserCreationChangeField]: hiddenChangeField("cms_creation_change_id"),
|
|
1266
|
-
[cmsUserSessionsRevokedChangeField]: hiddenChangeField("cms_sessions_revoked_change_id")
|
|
1267
|
-
},
|
|
1268
|
-
modelName: "auth_users",
|
|
1269
|
-
fields: {
|
|
1270
|
-
...timestampFields,
|
|
1271
|
-
emailVerified: "email_verified"
|
|
1272
|
-
}
|
|
1273
|
-
},
|
|
1274
|
-
session: {
|
|
1275
|
-
cookieCache: {
|
|
1276
|
-
enabled: false,
|
|
1277
|
-
strategy: "jwt"
|
|
1278
|
-
},
|
|
1279
|
-
modelName: "auth_sessions",
|
|
1280
|
-
fields: {
|
|
1281
|
-
...timestampFields,
|
|
1282
|
-
expiresAt: "expires_at",
|
|
1283
|
-
ipAddress: "ip_address",
|
|
1284
|
-
userAgent: "user_agent",
|
|
1285
|
-
userId: "user_id"
|
|
1286
|
-
}
|
|
1287
|
-
},
|
|
1288
|
-
account: {
|
|
1289
|
-
modelName: "auth_accounts",
|
|
1290
|
-
fields: {
|
|
1291
|
-
accessToken: "access_token",
|
|
1292
|
-
accessTokenExpiresAt: "access_token_expires_at",
|
|
1293
|
-
accountId: "account_id",
|
|
1294
|
-
...timestampFields,
|
|
1295
|
-
idToken: "id_token",
|
|
1296
|
-
providerId: "provider_id",
|
|
1297
|
-
refreshToken: "refresh_token",
|
|
1298
|
-
refreshTokenExpiresAt: "refresh_token_expires_at",
|
|
1299
|
-
userId: "user_id"
|
|
1300
|
-
}
|
|
1301
|
-
},
|
|
1302
|
-
verification: {
|
|
1303
|
-
modelName: "auth_verifications",
|
|
1304
|
-
fields: {
|
|
1305
|
-
...timestampFields,
|
|
1306
|
-
expiresAt: "expires_at"
|
|
1307
|
-
}
|
|
1308
|
-
},
|
|
1309
|
-
rateLimit: {
|
|
1310
|
-
enabled: true,
|
|
1311
|
-
storage: "database",
|
|
1312
|
-
modelName: "auth_rate_limits",
|
|
1313
|
-
fields: { lastRequest: "last_request" }
|
|
1314
|
-
},
|
|
1315
|
-
plugins: [admin({
|
|
1316
|
-
ac: cmsBetterAuthAccess,
|
|
1317
|
-
adminRoles: ["administrator"],
|
|
1318
|
-
defaultRole: "editor",
|
|
1319
|
-
roles: cmsBetterAuthRoles,
|
|
1320
|
-
schema: {
|
|
1321
|
-
session: { fields: { impersonatedBy: "impersonated_by" } },
|
|
1322
|
-
user: { fields: {
|
|
1323
|
-
banExpires: "ban_expires",
|
|
1324
|
-
banReason: "ban_reason",
|
|
1325
|
-
banned: "banned",
|
|
1326
|
-
role: "role"
|
|
1327
|
-
} }
|
|
1328
|
-
}
|
|
1329
|
-
})],
|
|
1330
|
-
secret: options.secret
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
function createCmsBetterAuth(options, activity) {
|
|
1334
|
-
return betterAuth(createCmsBetterAuthOptions(options, activity));
|
|
1335
|
-
}
|
|
1306
|
+
//#region ../auth/src/BetterAuth.ts
|
|
1336
1307
|
function betterAuthLayer(options) {
|
|
1308
|
+
if (options.basePath !== void 0 && options.basePath !== "/api/cms/auth") throw new Error(`Authentication basePath must be ${CMS_AUTHENTICATION_PATH}.`);
|
|
1337
1309
|
return Layer.effectContext(Effect.gen(function* () {
|
|
1338
1310
|
const crypto = yield* Crypto.Crypto;
|
|
1339
1311
|
const storage = yield* ActivityStore;
|
|
1340
|
-
const
|
|
1312
|
+
const runPromise = Effect.runPromiseWith(yield* Effect.context());
|
|
1313
|
+
const auth = createCmsBetterAuth(options, betterAuthActivityOptions(betterAuthActivityWriter(storage, crypto), crypto, runPromise));
|
|
1341
1314
|
const identity = betterAuthIdentity(auth);
|
|
1342
1315
|
const recovery = betterAuthRecovery(auth);
|
|
1343
1316
|
const initialization = betterAuthInitialIdentity(auth);
|
|
1344
1317
|
const basePath = options.basePath ?? "/api/cms/auth";
|
|
1345
|
-
const handler = AuthenticationHandler.of({ handle: rejectExternalAdminRoutes(auth.handler, basePath) });
|
|
1346
|
-
return Context.empty().pipe(Context.add(Authentication, identity.authentication), Context.add(IdentityAdministration, identity.administration), Context.add(IdentityRecovery, recovery.users), Context.add(InitialIdentity, initialization), Context.add(InitialIdentityRecovery, recovery.initial), Context.add(AuthenticationHandler, handler));
|
|
1347
|
-
}));
|
|
1348
|
-
}
|
|
1349
|
-
function rejectExternalAdminRoutes(handler, basePath) {
|
|
1350
|
-
const mountedAt = canonicalBasePath(basePath);
|
|
1351
|
-
return (request) => {
|
|
1352
|
-
const path = new URL(request.url).pathname;
|
|
1353
|
-
const relative = mountedAt === "/" ? path : path === mountedAt ? "/" : path.startsWith(`${mountedAt}/`) ? path.slice(mountedAt.length) : path;
|
|
1354
|
-
if (relative === "/admin" || relative.startsWith("/admin/")) return Promise.resolve(new Response(null, { status: 404 }));
|
|
1355
|
-
return handler(request);
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
function canonicalBasePath(basePath) {
|
|
1359
|
-
return (basePath.startsWith("/") ? basePath : `/${basePath}`).replace(/\/+$/u, "") || "/";
|
|
1360
|
-
}
|
|
1361
|
-
function hiddenChangeField(fieldName) {
|
|
1362
|
-
return {
|
|
1363
|
-
fieldName,
|
|
1364
|
-
input: false,
|
|
1365
|
-
required: false,
|
|
1366
|
-
returned: false,
|
|
1367
|
-
type: "string"
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
//#endregion
|
|
1372
|
-
//#region ../server/src/adapter/Readiness.ts
|
|
1373
|
-
var ReadinessFailed = class extends Schema.TaggedError()("Cms.ReadinessFailed", { cause: Schema.Defect() }) {};
|
|
1374
|
-
var Readiness = class extends Context.Service()("@internal/server/Readiness") {};
|
|
1375
|
-
|
|
1376
|
-
//#endregion
|
|
1377
|
-
//#region ../server/src/internal/working-documents/working-document-model.ts
|
|
1378
|
-
const WorkingDocumentGenerationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("WorkingDocumentGenerationId"));
|
|
1379
|
-
const WorkingDocumentUpdateSequence = Schema.Int.check(Schema.isGreaterThan(0)).pipe(Schema.brand("WorkingDocumentUpdateSequence"));
|
|
1380
|
-
const WorkingDocumentHighWatermark = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("WorkingDocumentHighWatermark"));
|
|
1381
|
-
var WorkingDocumentPersistenceError = class extends Schema.TaggedError()("WorkingDocuments.PersistenceError", {
|
|
1382
|
-
operation: Schema.NonEmptyString,
|
|
1383
|
-
cause: Schema.Defect()
|
|
1384
|
-
}) {};
|
|
1385
|
-
var WorkingDocumentSuperseded = class extends Schema.TaggedError()("WorkingDocuments.Superseded", {
|
|
1386
|
-
documentId: DocumentId,
|
|
1387
|
-
expectedGeneration: WorkingDocumentGenerationId,
|
|
1388
|
-
actualGeneration: Schema.optionalKey(WorkingDocumentGenerationId)
|
|
1389
|
-
}) {};
|
|
1390
|
-
var WorkingDocumentConflict = class extends Schema.TaggedError()("WorkingDocuments.Conflict", {
|
|
1391
|
-
documentId: DocumentId,
|
|
1392
|
-
operation: Schema.NonEmptyString,
|
|
1393
|
-
message: Schema.NonEmptyString
|
|
1394
|
-
}) {};
|
|
1395
|
-
|
|
1396
|
-
//#endregion
|
|
1397
|
-
//#region ../server/src/internal/working-documents/working-document.ts
|
|
1398
|
-
/** Durable identity for one exact recoverable Working Document generation. */
|
|
1399
|
-
const WorkingDocumentRef = Schema.Struct({
|
|
1400
|
-
documentId: DocumentId,
|
|
1401
|
-
generationId: WorkingDocumentGenerationId
|
|
1402
|
-
});
|
|
1403
|
-
var WorkingDocumentInvalid = class extends Schema.TaggedError()("WorkingDocuments.Invalid", {
|
|
1404
|
-
documentId: DocumentId,
|
|
1405
|
-
message: Schema.NonEmptyString,
|
|
1406
|
-
cause: Schema.Defect()
|
|
1407
|
-
}) {};
|
|
1408
|
-
var WorkingDocumentUnavailable = class extends Schema.TaggedError()("WorkingDocuments.Unavailable", {
|
|
1409
|
-
operation: Schema.NonEmptyString,
|
|
1410
|
-
cause: Schema.Defect()
|
|
1411
|
-
}) {};
|
|
1412
|
-
/** An authored or working baseline committed, but the publish workflow still needs recovery. */
|
|
1413
|
-
var WorkingDocumentCheckpointed = class extends Schema.TaggedError()("WorkingDocuments.Checkpointed", {
|
|
1414
|
-
checkpointVersion: DocumentVersion,
|
|
1415
|
-
documentId: DocumentId,
|
|
1416
|
-
result: Schema.optionalKey(DocumentMutationResult),
|
|
1417
|
-
stateVector: Schema.Uint8Array,
|
|
1418
|
-
cause: Schema.Defect()
|
|
1419
|
-
}) {};
|
|
1420
|
-
/** Signals that one exact Working Document generation changed or became invalid. */
|
|
1421
|
-
const WorkingDocumentChange = Schema.Struct({
|
|
1422
|
-
kind: Schema.Literals(["updated", "invalidated"]),
|
|
1423
|
-
ref: WorkingDocumentRef
|
|
1424
|
-
});
|
|
1425
|
-
var WorkingDocuments = class extends Context.Service()("@internal/server/WorkingDocuments") {};
|
|
1426
|
-
|
|
1427
|
-
//#endregion
|
|
1428
|
-
//#region ../document/src/internal/editor/render.ts
|
|
1429
|
-
const renderEditorMarkdown = Effect.fn("EditorDocument.renderMarkdown")((document) => {
|
|
1430
|
-
return Effect.tryPromise({
|
|
1431
|
-
try: () => renderMarkdown(toComarkDocument(withoutEditorMetadata(document))),
|
|
1432
|
-
catch: (cause) => new EditorRenderFailed({ cause })
|
|
1433
|
-
});
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
//#endregion
|
|
1437
|
-
//#region ../document/src/internal/document-engine/relocate-links.ts
|
|
1438
|
-
/** Rewrites only semantic page links, preserving bindings and unrelated content. */
|
|
1439
|
-
function rewriteDocumentRoutes(document, destinations, components = []) {
|
|
1440
|
-
let changed = false;
|
|
1441
|
-
function link(value) {
|
|
1442
|
-
const target = referenceTarget("a", /* @__PURE__ */ new Map([["href", value]]), document.path, void 0);
|
|
1443
|
-
if (target?.kind !== "document" || !Predicate.isString(value)) return value;
|
|
1444
|
-
const destination = destinations.get(target.path);
|
|
1445
|
-
const sourceMoved = destinations.has(document.path);
|
|
1446
|
-
if (!destination && (!sourceMoved || value.startsWith("/"))) return value;
|
|
1447
|
-
const suffix = value.match(/[?#].*$/u)?.[0] ?? "";
|
|
1448
|
-
const next = `${destination ?? target.path}${suffix}`;
|
|
1449
|
-
if (next !== value) changed = true;
|
|
1450
|
-
return next;
|
|
1451
|
-
}
|
|
1452
|
-
function properties(fields, values) {
|
|
1453
|
-
const result = mutableJsonObject(values);
|
|
1454
|
-
for (const field of fields) {
|
|
1455
|
-
const value = values[field.name] === void 0 ? field.defaultValue : values[field.name];
|
|
1456
|
-
if (field.input === "link") {
|
|
1457
|
-
const next = link(value);
|
|
1458
|
-
if (next !== void 0 && next !== value) result[field.name] = next;
|
|
1459
|
-
} else if (field.fields) {
|
|
1460
|
-
const next = Array.isArray(value) ? value.map((item) => Schema.is(Schema.JsonObject)(item) ? properties(field.fields ?? [], item) : item) : Schema.is(Schema.JsonObject)(value) ? properties(field.fields, value) : value;
|
|
1461
|
-
if (next !== void 0 && JSON.stringify(next) !== JSON.stringify(value)) result[field.name] = next;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
return result;
|
|
1465
|
-
}
|
|
1466
|
-
function visit(node, renderData) {
|
|
1467
|
-
if (Predicate.isString(node) || node[0] === null) return node;
|
|
1468
|
-
const [tag, attributes, ...children] = node;
|
|
1469
|
-
const projection = projectCmsEditorComponentAttributes(attributes, renderData);
|
|
1470
|
-
let props = properties(components.find((component) => component.name.toLowerCase() === tag.toLowerCase())?.authoring.props ?? [], projection.props);
|
|
1471
|
-
if (tag === "a") {
|
|
1472
|
-
const href = link(projection.props.href);
|
|
1473
|
-
if (href !== void 0) props = Object.assign({}, props, { href });
|
|
1474
|
-
}
|
|
1475
|
-
return [
|
|
1476
|
-
tag,
|
|
1477
|
-
JSON.stringify(props) === JSON.stringify(projection.props) ? attributes : encodeCmsEditorComponentAttributes(props, projection.bindings),
|
|
1478
|
-
...children.map((child) => visit(child, projection.renderData))
|
|
1479
|
-
];
|
|
1480
|
-
}
|
|
1481
|
-
const nodes = document.body.nodes.map((node) => visit(node, createCmsComarkRenderData(document.body)));
|
|
1482
|
-
return changed ? Object.assign({}, document.body, { nodes }) : void 0;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
//#endregion
|
|
1486
|
-
//#region ../document/src/DocumentPlanning.ts
|
|
1487
|
-
/** Derives one canonical Source path and public path without performing persistence. */
|
|
1488
|
-
function planDocumentCreation(configuration, input) {
|
|
1489
|
-
return Result.gen(function* () {
|
|
1490
|
-
const collection = configuration.types.find((candidate) => candidate.collection === input.collection);
|
|
1491
|
-
if (!collection) return yield* invalid$2(`Document collection "${input.collection}" does not exist.`, "collection");
|
|
1492
|
-
if (!collection.canCreate) return yield* invalid$2(`Document collection "${input.collection}" does not allow creation.`, "collection");
|
|
1493
|
-
const title = input.title.trim();
|
|
1494
|
-
if (!title) return yield* invalid$2("A document title is required.", "title");
|
|
1495
|
-
const locale = yield* resolveLocale(configuration, input.locale);
|
|
1496
|
-
const address = yield* parseDocumentAddress(collection.addressPattern).pipe(Result.mapError(addressInvalid));
|
|
1497
|
-
const relativePath = yield* resolveRelativePath(collection, input.location);
|
|
1498
|
-
const data = Object.fromEntries(address.tokens.flatMap((token) => {
|
|
1499
|
-
if (token.name === "slug" && relativePath) return [[token.name, relativePath.split("/").at(-1) ?? relativePath]];
|
|
1500
|
-
return [];
|
|
1501
|
-
}));
|
|
1502
|
-
const relativeKey = yield* normalizePublicPath(relativePath).pipe(Result.mapError(addressInvalid));
|
|
1503
|
-
const key = yield* resolveDocumentAddress(address, relativeKey, data).pipe(Result.mapError(addressInvalid));
|
|
1504
|
-
const sourcePath = documentSourcePath(collection, relativePath, locale);
|
|
1505
|
-
const path = localizeDocumentPath(key, locale, configuration.locales);
|
|
1506
|
-
return Object.assign({
|
|
1507
|
-
collection,
|
|
1508
|
-
data,
|
|
1509
|
-
key,
|
|
1510
|
-
path,
|
|
1511
|
-
sourcePath,
|
|
1512
|
-
title
|
|
1513
|
-
}, locale === void 0 ? void 0 : { locale });
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
/** Creates the canonical initial editor source, including configured field defaults. */
|
|
1517
|
-
const createInitialDocumentSource = Effect.fn("DocumentPlanning.createInitialSource")(function* (plan) {
|
|
1518
|
-
const body = CmsMarkdownDocument.make({
|
|
1519
|
-
frontmatter: initialFrontmatter(plan),
|
|
1520
|
-
meta: {},
|
|
1521
|
-
nodes: [[
|
|
1522
|
-
"h1",
|
|
1523
|
-
{},
|
|
1524
|
-
plan.title
|
|
1525
|
-
]]
|
|
1526
|
-
});
|
|
1527
|
-
return {
|
|
1528
|
-
body,
|
|
1529
|
-
markdown: yield* renderEditorMarkdown(body)
|
|
1530
|
-
};
|
|
1531
|
-
});
|
|
1532
|
-
/** Derives the canonical destination Source path while preserving the Document's collection and locale. */
|
|
1533
|
-
function planDocumentMove(configuration, document, location) {
|
|
1534
|
-
return Result.gen(function* () {
|
|
1535
|
-
const collection = configuration.types.find((candidate) => candidate.collection === document.collection);
|
|
1536
|
-
if (!collection) return yield* invalid$2(`Document collection "${document.collection}" does not exist.`, "documentId");
|
|
1537
|
-
return documentSourcePath(collection, yield* resolveRelativePath(collection, location), document.locale);
|
|
1538
|
-
});
|
|
1539
|
-
}
|
|
1540
|
-
/** Derives a collection-relative hierarchy path from a canonical document summary. */
|
|
1541
|
-
function documentRelativePath(collection, entry) {
|
|
1542
|
-
let sourcePath = entry.sourcePath;
|
|
1543
|
-
if (entry.document.locale && sourcePath.startsWith(`${entry.document.locale}/`)) sourcePath = sourcePath.slice(entry.document.locale.length + 1);
|
|
1544
|
-
if (collection.directory !== "." && sourcePath.startsWith(`${collection.directory}/`)) sourcePath = sourcePath.slice(collection.directory.length + 1);
|
|
1545
|
-
sourcePath = sourcePath.replace(/\.(?:md|mdc)$/iu, "");
|
|
1546
|
-
return sourcePath.toLocaleLowerCase() === "index" ? void 0 : sourcePath.replace(/\/index$/iu, "");
|
|
1547
|
-
}
|
|
1548
|
-
function documentSourcePath(collection, relativePath, locale) {
|
|
1549
|
-
const sourceName = relativePath ? `${relativePath}.md` : "index.md";
|
|
1550
|
-
return SourcePath.make([
|
|
1551
|
-
locale,
|
|
1552
|
-
collection.directory === "." ? void 0 : collection.directory,
|
|
1553
|
-
sourceName
|
|
1554
|
-
].filter((value) => value !== void 0).join("/"));
|
|
1555
|
-
}
|
|
1556
|
-
function initialFrontmatter(plan) {
|
|
1557
|
-
let frontmatter = {
|
|
1558
|
-
...plan.data,
|
|
1559
|
-
title: plan.title
|
|
1560
|
-
};
|
|
1561
|
-
for (const field of plan.collection.authoringFields) if (field.defaultValue !== void 0 && nestedValue$1(frontmatter, field.path) === void 0) frontmatter = setNestedValue(frontmatter, field.path.split("."), field.defaultValue);
|
|
1562
|
-
return frontmatter;
|
|
1563
|
-
}
|
|
1564
|
-
function nestedValue$1(value, path) {
|
|
1565
|
-
return path.split(".").reduce((current, segment) => isJsonObject$4(current) ? current[segment] : void 0, value);
|
|
1566
|
-
}
|
|
1567
|
-
function setNestedValue(target, path, value) {
|
|
1568
|
-
const [segment, ...rest] = path;
|
|
1569
|
-
if (!segment) return target;
|
|
1570
|
-
if (rest.length === 0) return Object.assign({}, target, { [segment]: value });
|
|
1571
|
-
const current = target[segment];
|
|
1572
|
-
return Object.assign({}, target, { [segment]: setNestedValue(isJsonObject$4(current) ? current : {}, rest, value) });
|
|
1573
|
-
}
|
|
1574
|
-
const isJsonObject$4 = Schema.is(Schema.JsonObject);
|
|
1575
|
-
function resolveLocale(configuration, requested) {
|
|
1576
|
-
const locales = configuration.locales;
|
|
1577
|
-
if (!locales) return requested === void 0 ? Result.succeed(void 0) : invalid$2(`Document locale "${requested}" is not configured.`, "locale");
|
|
1578
|
-
const locale = requested ?? locales.defaultLocale;
|
|
1579
|
-
return locales.locales.includes(locale) ? Result.succeed(locale) : invalid$2(`Document locale "${locale}" does not exist.`, "locale");
|
|
1580
|
-
}
|
|
1581
|
-
function resolveRelativePath(collection, location) {
|
|
1582
|
-
if (location.kind === "home") return collection.addressPattern === "/:path*" ? Result.succeed("") : invalid$2("Only a root catch-all collection can use the home location.", "location");
|
|
1583
|
-
if (location.parent !== void 0 && !collection.hierarchy) return invalid$2(`Document collection "${collection.collection}" does not allow hierarchy.`, "location.parent");
|
|
1584
|
-
return normalizeDocumentRelativePath([location.parent, location.slug].filter((value) => value !== void 0).join("/")).pipe(Result.mapError((error) => addressInvalid({
|
|
1585
|
-
...error,
|
|
1586
|
-
field: "location"
|
|
1587
|
-
})));
|
|
1588
|
-
}
|
|
1589
|
-
function addressInvalid(error) {
|
|
1590
|
-
return new DocumentValidationFailed({ issues: [validationIssue(error.message, error.field)] });
|
|
1591
|
-
}
|
|
1592
|
-
function invalid$2(message, field) {
|
|
1593
|
-
return Result.fail(new DocumentValidationFailed({ issues: [validationIssue(message, field)] }));
|
|
1594
|
-
}
|
|
1595
|
-
function validationIssue(message, field) {
|
|
1596
|
-
return field === void 0 ? {
|
|
1597
|
-
code: "document.invalid",
|
|
1598
|
-
message,
|
|
1599
|
-
severity: "error"
|
|
1600
|
-
} : {
|
|
1601
|
-
code: "document.invalid",
|
|
1602
|
-
field,
|
|
1603
|
-
message,
|
|
1604
|
-
severity: "error"
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
/** Updates route-owned frontmatter when relocating a page in a slug-addressed collection. */
|
|
1608
|
-
const relocateDocumentMarkdown = Effect.fn("DocumentPlanning.relocateMarkdown")(function* (configuration, document, location, markdown) {
|
|
1609
|
-
const collection = configuration.types.find((type) => type.collection === document.collection);
|
|
1610
|
-
if (!collection || !collection.addressPattern.includes(":slug") || location.kind === "home") return markdown;
|
|
1611
|
-
return yield* renderEditorMarkdown({
|
|
1612
|
-
...document.body,
|
|
1613
|
-
frontmatter: {
|
|
1614
|
-
...document.body.frontmatter,
|
|
1615
|
-
slug: location.slug
|
|
1616
|
-
}
|
|
1617
|
-
});
|
|
1618
|
-
});
|
|
1619
|
-
|
|
1620
|
-
//#endregion
|
|
1621
|
-
//#region ../server/src/internal/documents/document-state.ts
|
|
1622
|
-
const isDocumentCompilationFailed = Schema.is(DocumentCompilationFailed);
|
|
1623
|
-
function makeDocumentState(authored, published, snapshot) {
|
|
1624
|
-
const activeById = /* @__PURE__ */ new Map();
|
|
1625
|
-
for (const source of snapshot.active) {
|
|
1626
|
-
if (activeById.has(source.documentId)) throw new Error(`Document "${source.documentId}" appears more than once in the repository snapshot.`);
|
|
1627
|
-
activeById.set(source.documentId, source);
|
|
1628
|
-
}
|
|
1629
|
-
const authoredById = /* @__PURE__ */ new Map();
|
|
1630
|
-
for (const document of authored.documents) {
|
|
1631
|
-
if (authoredById.has(document.id)) throw new Error(`Compiled document "${document.id}" appears more than once in the authored catalog.`);
|
|
1632
|
-
if (!activeById.has(document.id)) throw new Error(`Compiled document "${document.id}" is missing from the repository snapshot.`);
|
|
1633
|
-
authoredById.set(document.id, document);
|
|
1634
|
-
}
|
|
1635
|
-
for (const documentId of activeById.keys()) if (!authoredById.has(documentId)) throw new Error(`Compiled document "${documentId}" is missing from the repository snapshot.`);
|
|
1636
|
-
return {
|
|
1637
|
-
activeById,
|
|
1638
|
-
authored,
|
|
1639
|
-
authoredById,
|
|
1640
|
-
published,
|
|
1641
|
-
snapshot
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
const compileDocumentState = Effect.fn("DocumentState.compile")(function* (engine, snapshot, previous) {
|
|
1645
|
-
return makeDocumentState(yield* engine.compileAll({
|
|
1646
|
-
previous: previous?.authored,
|
|
1647
|
-
sources: authoredSources(snapshot)
|
|
1648
|
-
}), yield* engine.compileAll({
|
|
1649
|
-
previous: previous?.published,
|
|
1650
|
-
sources: publishedSources(snapshot)
|
|
1651
|
-
}), snapshot);
|
|
1652
|
-
});
|
|
1653
|
-
function authoredSources(snapshot) {
|
|
1654
|
-
return snapshot.active.map((source) => ({
|
|
1655
|
-
documentId: source.documentId,
|
|
1656
|
-
draft: source.publishedVersion !== source.version,
|
|
1657
|
-
markdown: source.markdown,
|
|
1658
|
-
sourcePath: source.sourcePath
|
|
1659
|
-
}));
|
|
1660
|
-
}
|
|
1661
|
-
function publishedSources(snapshot) {
|
|
1662
|
-
return snapshot.published.map((source) => ({
|
|
1663
|
-
documentId: source.documentId,
|
|
1664
|
-
draft: false,
|
|
1665
|
-
markdown: source.markdown,
|
|
1666
|
-
sourcePath: source.sourcePath
|
|
1667
|
-
}));
|
|
1668
|
-
}
|
|
1669
|
-
const compileValidCatalog = Effect.fn("DocumentState.compileValid")(function* (engine, sources, previous) {
|
|
1670
|
-
const result = yield* Effect.result(engine.compileAll({
|
|
1671
|
-
previous,
|
|
1672
|
-
sources
|
|
1673
|
-
}));
|
|
1674
|
-
if (Result.isFailure(result)) return yield* new DocumentValidationFailed({ issues: compilationIssues(result.failure) });
|
|
1675
|
-
const errors = result.success.issues.filter((issue) => issue.severity === "error");
|
|
1676
|
-
if (errors.length > 0) return yield* new DocumentValidationFailed({ issues: errors });
|
|
1677
|
-
return result.success;
|
|
1678
|
-
});
|
|
1679
|
-
const validateDocumentSources = Effect.fn("DocumentState.validate")(function* (engine, sources, sourcePath, previous) {
|
|
1680
|
-
const result = yield* Effect.result(engine.compileAll({
|
|
1681
|
-
previous,
|
|
1682
|
-
sources
|
|
1683
|
-
}));
|
|
1684
|
-
if (Result.isFailure(result)) return DocumentValidationResult.make({
|
|
1685
|
-
issues: compilationIssues(result.failure),
|
|
1686
|
-
valid: false
|
|
1687
|
-
});
|
|
1688
|
-
const catalog = result.success;
|
|
1689
|
-
const document = catalog.documents.find((candidate) => candidate.source.path === sourcePath);
|
|
1690
|
-
const issues = catalog.issues;
|
|
1691
|
-
const validation = {
|
|
1692
|
-
issues,
|
|
1693
|
-
valid: document !== void 0 && !issues.some((issue) => issue.severity === "error")
|
|
1694
|
-
};
|
|
1695
|
-
return DocumentValidationResult.make(Object.assign(validation, document === void 0 ? void 0 : { document }));
|
|
1696
|
-
});
|
|
1697
|
-
function authoringEntries(state, query) {
|
|
1698
|
-
return state.snapshot.active.flatMap((source) => {
|
|
1699
|
-
const document = state.authoredById.get(source.documentId);
|
|
1700
|
-
if (!document) throw new Error(`Compiled document "${source.documentId}" is missing from the repository snapshot.`);
|
|
1701
|
-
if (query.collection !== void 0 && document.collection !== query.collection) return [];
|
|
1702
|
-
if (query.locale !== void 0 && document.locale !== query.locale) return [];
|
|
1703
|
-
return [DocumentSummary.make({
|
|
1704
|
-
...browseMetadata(state, source),
|
|
1705
|
-
document,
|
|
1706
|
-
sourcePath: source.sourcePath,
|
|
1707
|
-
version: source.version
|
|
1708
|
-
})];
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
function deletedEntries(state) {
|
|
1712
|
-
return state.snapshot.deleted.map((source) => DeletedDocument.make({
|
|
1713
|
-
...source.title ? { title: source.title } : void 0,
|
|
1714
|
-
...source.path ? { path: source.path } : void 0,
|
|
1715
|
-
...source.trashBatchId ? { trashBatchId: source.trashBatchId } : void 0,
|
|
1716
|
-
documentId: source.documentId,
|
|
1717
|
-
deletedAt: source.deletedAt,
|
|
1718
|
-
sourcePath: source.sourcePath,
|
|
1719
|
-
version: source.version
|
|
1720
|
-
}));
|
|
1721
|
-
}
|
|
1722
|
-
function entryById(state, documentId) {
|
|
1723
|
-
const source = state.activeById.get(documentId);
|
|
1724
|
-
const document = state.authoredById.get(documentId);
|
|
1725
|
-
return source && document ? DocumentSummary.make({
|
|
1726
|
-
...browseMetadata(state, source),
|
|
1727
|
-
document,
|
|
1728
|
-
sourcePath: source.sourcePath,
|
|
1729
|
-
version: source.version
|
|
1730
|
-
}) : void 0;
|
|
1731
|
-
}
|
|
1732
|
-
function compilationIssues(error) {
|
|
1733
|
-
if (isDocumentCompilationFailed(error)) return error.issues;
|
|
1734
|
-
return [{
|
|
1735
|
-
code: "markdown.parse",
|
|
1736
|
-
message: errorMessage(error.cause),
|
|
1737
|
-
severity: "error",
|
|
1738
|
-
sourcePath: error.sourcePath
|
|
1739
|
-
}];
|
|
1740
|
-
}
|
|
1741
|
-
function errorMessage(cause) {
|
|
1742
|
-
return cause instanceof Error && cause.message ? cause.message : "Markdown could not be parsed.";
|
|
1743
|
-
}
|
|
1744
|
-
function browseMetadata(state, source) {
|
|
1745
|
-
const live = state.published.documents.find((document) => document.id === source.documentId);
|
|
1746
|
-
const metadata = {};
|
|
1747
|
-
if (source.createdBy) metadata.createdBy = source.createdBy;
|
|
1748
|
-
if (source.editedBy) metadata.editedBy = source.editedBy;
|
|
1749
|
-
if (source.editedAt) metadata.editedAt = source.editedAt;
|
|
1750
|
-
if (source.publishedVersion) metadata.publishedVersion = source.publishedVersion;
|
|
1751
|
-
if (live) metadata.publishedPath = live.path;
|
|
1752
|
-
return metadata;
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
//#endregion
|
|
1756
|
-
//#region ../server/src/internal/documents/document-tree-mutation.ts
|
|
1757
|
-
/** Selects by route boundaries, never by title or a raw prefix such as /about matching /about-us. */
|
|
1758
|
-
function subtreeIds(state, documentId) {
|
|
1759
|
-
const parent = state.authoredById.get(documentId);
|
|
1760
|
-
if (!parent) return [documentId];
|
|
1761
|
-
return state.authored.documents.filter((page) => page.id === parent.id || parent.key !== "/" && page.collection === parent.collection && page.locale === parent.locale && page.key.startsWith(`${parent.key}/`)).map((page) => page.id);
|
|
1762
|
-
}
|
|
1763
|
-
const prepareTreeMutation = Effect.fn("Documents.prepareTreeMutation")(function* (dependencies, state, command) {
|
|
1764
|
-
const { configuration, engine } = dependencies;
|
|
1765
|
-
const input = command.input;
|
|
1766
|
-
const root = command.action === "document.undelete" ? state.snapshot.deleted.find((source) => source.documentId === input.documentId) : state.activeById.get(input.documentId);
|
|
1767
|
-
if (!root) return yield* new DocumentNotFound({ documentId: input.documentId });
|
|
1768
|
-
if (root.version !== input.expectedVersion) return yield* new DocumentVersionConflict({
|
|
1769
|
-
documentId: root.documentId,
|
|
1770
|
-
actualVersion: root.version,
|
|
1771
|
-
expectedVersion: input.expectedVersion
|
|
1772
|
-
});
|
|
1773
|
-
let sources;
|
|
1774
|
-
if (command.action === "document.undelete") {
|
|
1775
|
-
const deleted = state.snapshot.deleted.find((source) => source.documentId === root.documentId);
|
|
1776
|
-
const prefix = root.sourcePath.replace(/(?:\/index)?\.(?:md|mdc)$/iu, "");
|
|
1777
|
-
sources = state.snapshot.deleted.filter((source) => source.documentId === root.documentId || deleted && deleted.trashBatchId && source.trashBatchId === deleted.trashBatchId && source.sourcePath.startsWith(`${prefix}/`));
|
|
1778
|
-
} else {
|
|
1779
|
-
const ids = new Set(command.action === "document.duplicate" && !command.input.includeChildren ? [root.documentId] : subtreeIds(state, root.documentId));
|
|
1780
|
-
sources = state.snapshot.active.filter((source) => ids.has(source.documentId));
|
|
1781
|
-
}
|
|
1782
|
-
sources = [root, ...sources.filter((source) => source.documentId !== root.documentId)];
|
|
1783
|
-
const plans = [];
|
|
1784
|
-
const parent = state.authoredById.get(root.documentId);
|
|
1785
|
-
for (const source of sources) {
|
|
1786
|
-
if (command.action === "document.remove" || command.action === "document.undelete") {
|
|
1787
|
-
const page = state.authoredById.get(source.documentId);
|
|
1788
|
-
plans.push({
|
|
1789
|
-
...command.action === "document.remove" ? {
|
|
1790
|
-
trashBatchId: root.version,
|
|
1791
|
-
title: page?.title,
|
|
1792
|
-
path: page?.path
|
|
1793
|
-
} : void 0,
|
|
1794
|
-
action: command.action,
|
|
1795
|
-
documentId: source.documentId,
|
|
1796
|
-
expectedVersion: source.version,
|
|
1797
|
-
markdown: source.markdown,
|
|
1798
|
-
sourcePath: source.sourcePath
|
|
1799
|
-
});
|
|
1800
|
-
continue;
|
|
1801
|
-
}
|
|
1802
|
-
const page = state.authoredById.get(source.documentId);
|
|
1803
|
-
const collection = configuration.types.find((type) => type.collection === page?.collection);
|
|
1804
|
-
if (!page || !parent || !collection) return yield* new DocumentNotFound({ documentId: source.documentId });
|
|
1805
|
-
if (command.action === "document.duplicate" && !collection.canCreate) return yield* invalid$1("This collection does not allow new pages.");
|
|
1806
|
-
const destination = command.input.location;
|
|
1807
|
-
if (parent.key === "/" && command.action === "document.move") return yield* invalid$1("The home page cannot be moved.");
|
|
1808
|
-
const rootRelative = documentRelativePath(collection, {
|
|
1809
|
-
document: parent,
|
|
1810
|
-
sourcePath: root.sourcePath
|
|
1811
|
-
}) ?? "";
|
|
1812
|
-
const nextRelative = destination.kind === "home" ? "" : [destination.parent, destination.slug].filter(Boolean).join("/");
|
|
1813
|
-
if (rootRelative && (nextRelative === rootRelative || nextRelative.startsWith(`${rootRelative}/`))) return yield* invalid$1("Choose a destination outside this page and its subpages.");
|
|
1814
|
-
const relative = documentRelativePath(collection, {
|
|
1815
|
-
document: page,
|
|
1816
|
-
sourcePath: source.sourcePath
|
|
1817
|
-
}) ?? "";
|
|
1818
|
-
const segments = `${nextRelative}${source.documentId === root.documentId ? "" : relative.slice(rootRelative.length)}`.split("/").filter(Boolean);
|
|
1819
|
-
const slug = segments.pop();
|
|
1820
|
-
const location = slug ? Object.assign({
|
|
1821
|
-
kind: "entry",
|
|
1822
|
-
slug: yield* decodeSlug(slug)
|
|
1823
|
-
}, segments.length ? { parent: yield* decodeParent(segments.join("/")) } : void 0) : { kind: "home" };
|
|
1824
|
-
const sourcePath = yield* Effect.fromResult(planDocumentMove(configuration, page, location));
|
|
1825
|
-
const markdown = yield* relocateDocumentMarkdown(configuration, page, location, source.markdown).pipe(Effect.mapError(() => invalid$1("The page could not be copied to its new URL.")));
|
|
1826
|
-
if (command.action === "document.duplicate") {
|
|
1827
|
-
const documentId = yield* command.nextDocumentId;
|
|
1828
|
-
plans.push({
|
|
1829
|
-
action: "document.create",
|
|
1830
|
-
documentId,
|
|
1831
|
-
markdown,
|
|
1832
|
-
sourcePath
|
|
1833
|
-
});
|
|
1834
|
-
} else plans.push({
|
|
1835
|
-
action: "document.move",
|
|
1836
|
-
documentId: source.documentId,
|
|
1837
|
-
expectedVersion: source.version,
|
|
1838
|
-
markdown,
|
|
1839
|
-
sourcePath,
|
|
1840
|
-
publishedVersion: source.publishedVersion
|
|
1841
|
-
});
|
|
1842
|
-
}
|
|
1843
|
-
const changed = new Set(plans.map((plan) => plan.documentId));
|
|
1844
|
-
const nextSources = authoredSources(state.snapshot).filter((source) => !changed.has(source.documentId));
|
|
1845
|
-
if (command.action !== "document.remove") for (const plan of plans) {
|
|
1846
|
-
if (nextSources.some((source) => source.sourcePath === plan.sourcePath)) return yield* new DocumentPathConflict({ sourcePath: plan.sourcePath });
|
|
1847
|
-
nextSources.push({
|
|
1848
|
-
documentId: plan.documentId,
|
|
1849
|
-
draft: true,
|
|
1850
|
-
markdown: plan.markdown,
|
|
1851
|
-
sourcePath: plan.sourcePath
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
if (command.action === "document.move" || command.action === "document.duplicate") {
|
|
1855
|
-
const preliminary = yield* engine.compileAll({
|
|
1856
|
-
sources: nextSources,
|
|
1857
|
-
previous: state.authored
|
|
1858
|
-
});
|
|
1859
|
-
const destinations = /* @__PURE__ */ new Map();
|
|
1860
|
-
for (let index = 0; index < sources.length; index++) {
|
|
1861
|
-
const original = sources[index];
|
|
1862
|
-
const plan = plans[index];
|
|
1863
|
-
const oldPage = original && state.authoredById.get(original.documentId);
|
|
1864
|
-
const newPage = plan && preliminary.documents.find((document) => document.id === plan.documentId);
|
|
1865
|
-
if (oldPage && newPage) destinations.set(oldPage.path, newPage.path);
|
|
1866
|
-
}
|
|
1867
|
-
const originals = command.action === "document.move" ? state.snapshot.active : sources;
|
|
1868
|
-
for (const source of originals) {
|
|
1869
|
-
const page = state.authoredById.get(source.documentId);
|
|
1870
|
-
if (!page) continue;
|
|
1871
|
-
const body = rewriteDocumentRoutes(page, destinations, dependencies.components);
|
|
1872
|
-
if (!body) continue;
|
|
1873
|
-
const index = command.action === "document.duplicate" ? sources.findIndex((item) => item.documentId === source.documentId) : plans.findIndex((plan) => plan.documentId === source.documentId);
|
|
1874
|
-
const existing = index >= 0 ? plans[index] : void 0;
|
|
1875
|
-
const projected = existing && preliminary.documents.find((document) => document.id === existing.documentId);
|
|
1876
|
-
const markdown = yield* renderEditorMarkdown(Object.assign({}, body, { frontmatter: projected?.body.frontmatter ?? body.frontmatter })).pipe(Effect.mapError(() => invalid$1("Page links could not be updated.")));
|
|
1877
|
-
if (existing) plans[index] = {
|
|
1878
|
-
...existing,
|
|
1879
|
-
markdown
|
|
1880
|
-
};
|
|
1881
|
-
else plans.push({
|
|
1882
|
-
action: "document.update",
|
|
1883
|
-
documentId: source.documentId,
|
|
1884
|
-
expectedVersion: source.version,
|
|
1885
|
-
sourcePath: source.sourcePath,
|
|
1886
|
-
markdown,
|
|
1887
|
-
publishedVersion: source.publishedVersion
|
|
1888
|
-
});
|
|
1889
|
-
}
|
|
1890
|
-
for (const plan of plans) {
|
|
1891
|
-
const index = nextSources.findIndex((source) => source.documentId === plan.documentId);
|
|
1892
|
-
const updated = {
|
|
1893
|
-
documentId: plan.documentId,
|
|
1894
|
-
sourcePath: plan.sourcePath,
|
|
1895
|
-
markdown: plan.markdown,
|
|
1896
|
-
draft: true
|
|
1897
|
-
};
|
|
1898
|
-
if (index >= 0) nextSources[index] = updated;
|
|
1899
|
-
else nextSources.push(updated);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
const first = plans[0];
|
|
1903
|
-
if (!first) return yield* Effect.die(/* @__PURE__ */ new Error("Empty subtree mutation."));
|
|
1904
|
-
return {
|
|
1905
|
-
authored: yield* compileValidCatalog(engine, nextSources, state.authored),
|
|
1906
|
-
published: command.action === "document.remove" ? yield* compileValidCatalog(engine, publishedSources(state.snapshot).filter((source) => !changed.has(source.documentId)), state.published) : state.published,
|
|
1907
|
-
commitPlan: first,
|
|
1908
|
-
additionalPlans: plans.slice(1)
|
|
1909
|
-
};
|
|
1910
|
-
});
|
|
1911
|
-
const decodeSlug = (value) => Schema.decodeEffect(Slug)(value).pipe(Effect.mapError(() => invalid$1("Invalid URL ending.")));
|
|
1912
|
-
const decodeParent = (value) => Schema.decodeEffect(RelativeDocumentPath)(value).pipe(Effect.mapError(() => invalid$1("Invalid parent URL.")));
|
|
1913
|
-
function invalid$1(message) {
|
|
1914
|
-
return new DocumentValidationFailed({ issues: [{
|
|
1915
|
-
code: "document.invalid",
|
|
1916
|
-
severity: "error",
|
|
1917
|
-
message
|
|
1918
|
-
}] });
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
//#endregion
|
|
1922
|
-
//#region ../server/src/internal/documents/document-lifecycle.ts
|
|
1923
|
-
const PreparedDocumentMutation = Data.taggedEnum();
|
|
1924
|
-
const prepareDocumentMutation = Effect.fn("DocumentLifecycle.prepare")(function* (dependencies, state, command) {
|
|
1925
|
-
const { engine, repository } = dependencies;
|
|
1926
|
-
if (command.action === "document.move" || command.action === "document.remove" || command.action === "document.undelete" || command.action === "document.duplicate") return PreparedDocumentMutation.Commit(yield* prepareTreeMutation(dependencies, state, command));
|
|
1927
|
-
const authored = authoredSources(state.snapshot);
|
|
1928
|
-
const published = publishedSources(state.snapshot);
|
|
1929
|
-
switch (command.action) {
|
|
1930
|
-
case "document.create": {
|
|
1931
|
-
yield* ensurePathAvailable(state, command.sourcePath);
|
|
1932
|
-
const nextAuthored = [...authored, {
|
|
1933
|
-
documentId: command.documentId,
|
|
1934
|
-
draft: true,
|
|
1935
|
-
markdown: command.markdown,
|
|
1936
|
-
sourcePath: command.sourcePath
|
|
1937
|
-
}];
|
|
1938
|
-
return PreparedDocumentMutation.Commit({
|
|
1939
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1940
|
-
published: state.published,
|
|
1941
|
-
commitPlan: {
|
|
1942
|
-
action: command.action,
|
|
1943
|
-
documentId: command.documentId,
|
|
1944
|
-
markdown: command.markdown,
|
|
1945
|
-
sourcePath: command.sourcePath
|
|
1946
|
-
}
|
|
1947
|
-
});
|
|
1948
|
-
}
|
|
1949
|
-
case "document.update": {
|
|
1950
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1951
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1952
|
-
const nextAuthored = replaceSource(authored, {
|
|
1953
|
-
documentId: current.documentId,
|
|
1954
|
-
draft: true,
|
|
1955
|
-
markdown: command.input.markdown,
|
|
1956
|
-
sourcePath: current.sourcePath
|
|
1957
|
-
});
|
|
1958
|
-
return PreparedDocumentMutation.Commit({
|
|
1959
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1960
|
-
published: state.published,
|
|
1961
|
-
commitPlan: {
|
|
1962
|
-
action: command.action,
|
|
1963
|
-
documentId: current.documentId,
|
|
1964
|
-
expectedVersion: command.input.expectedVersion,
|
|
1965
|
-
markdown: command.input.markdown,
|
|
1966
|
-
publishedVersion: current.publishedVersion,
|
|
1967
|
-
sourcePath: current.sourcePath
|
|
1968
|
-
}
|
|
1969
|
-
});
|
|
1970
|
-
}
|
|
1971
|
-
case "document.restore": {
|
|
1972
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1973
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1974
|
-
const historical = yield* repository.revision(current.documentId, command.input.revisionId);
|
|
1975
|
-
const nextAuthored = replaceSource(authored, {
|
|
1976
|
-
documentId: current.documentId,
|
|
1977
|
-
draft: true,
|
|
1978
|
-
markdown: historical.markdown,
|
|
1979
|
-
sourcePath: current.sourcePath
|
|
1980
|
-
});
|
|
1981
|
-
return PreparedDocumentMutation.Commit({
|
|
1982
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1983
|
-
published: state.published,
|
|
1984
|
-
commitPlan: {
|
|
1985
|
-
action: command.action,
|
|
1986
|
-
documentId: current.documentId,
|
|
1987
|
-
expectedVersion: command.input.expectedVersion,
|
|
1988
|
-
markdown: historical.markdown,
|
|
1989
|
-
publishedVersion: current.publishedVersion,
|
|
1990
|
-
revisionId: command.input.revisionId,
|
|
1991
|
-
sourcePath: current.sourcePath
|
|
1992
|
-
}
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
case "document.unpublish": {
|
|
1996
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1997
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1998
|
-
return PreparedDocumentMutation.Commit({
|
|
1999
|
-
authored: yield* compileValidCatalog(engine, replaceSource(authored, {
|
|
2000
|
-
documentId: current.documentId,
|
|
2001
|
-
draft: true,
|
|
2002
|
-
markdown: current.markdown,
|
|
2003
|
-
sourcePath: current.sourcePath
|
|
2004
|
-
}), state.authored),
|
|
2005
|
-
published: yield* compileValidCatalog(engine, withoutDocument(published, current.documentId), state.published),
|
|
2006
|
-
commitPlan: {
|
|
2007
|
-
action: command.action,
|
|
2008
|
-
documentId: current.documentId,
|
|
2009
|
-
expectedVersion: current.version,
|
|
2010
|
-
markdown: current.markdown,
|
|
2011
|
-
sourcePath: current.sourcePath,
|
|
2012
|
-
revisionId: current.revisionId
|
|
2013
|
-
}
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
case "document.publish": {
|
|
2017
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
2018
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
2019
|
-
if (current.publishedVersion === current.version) {
|
|
2020
|
-
const entry = entryById(state, current.documentId);
|
|
2021
|
-
const result = {
|
|
2022
|
-
action: "document.publish",
|
|
2023
|
-
affectedDocumentIds: [current.documentId],
|
|
2024
|
-
documentId: current.documentId,
|
|
2025
|
-
generation: state.snapshot.generation,
|
|
2026
|
-
revisionId: current.revisionId,
|
|
2027
|
-
sourcePath: current.sourcePath,
|
|
2028
|
-
version: current.version
|
|
2029
|
-
};
|
|
2030
|
-
return PreparedDocumentMutation.Unchanged({ result: DocumentMutationResult.make(Object.assign(result, entry === void 0 ? void 0 : { entry })) });
|
|
2031
|
-
}
|
|
2032
|
-
const source = {
|
|
2033
|
-
documentId: current.documentId,
|
|
2034
|
-
draft: false,
|
|
2035
|
-
markdown: current.markdown,
|
|
2036
|
-
sourcePath: current.sourcePath
|
|
2037
|
-
};
|
|
2038
|
-
return PreparedDocumentMutation.Commit({
|
|
2039
|
-
authored: yield* compileValidCatalog(engine, replaceSource(authored, source), state.authored),
|
|
2040
|
-
published: yield* compileValidCatalog(engine, replaceSource(published, source), state.published),
|
|
2041
|
-
commitPlan: {
|
|
2042
|
-
action: command.action,
|
|
2043
|
-
documentId: current.documentId,
|
|
2044
|
-
expectedVersion: command.input.expectedVersion,
|
|
2045
|
-
markdown: current.markdown,
|
|
2046
|
-
revisionId: current.revisionId,
|
|
2047
|
-
sourcePath: current.sourcePath
|
|
2048
|
-
}
|
|
2049
|
-
});
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
});
|
|
2053
|
-
const validateDocumentUpdate = Effect.fn("DocumentLifecycle.validateUpdate")(function* (engine, state, input) {
|
|
2054
|
-
const current = yield* requireActive(state, input.documentId);
|
|
2055
|
-
yield* requireVersion(current, input.expectedVersion);
|
|
2056
|
-
const source = {
|
|
2057
|
-
documentId: current.documentId,
|
|
2058
|
-
draft: true,
|
|
2059
|
-
markdown: input.markdown,
|
|
2060
|
-
sourcePath: current.sourcePath
|
|
2061
|
-
};
|
|
2062
|
-
return yield* validateDocumentSources(engine, replaceSource(authoredSources(state.snapshot), source), current.sourcePath, state.authored);
|
|
2063
|
-
});
|
|
2064
|
-
function requireActive(state, documentId) {
|
|
2065
|
-
const source = state.activeById.get(documentId);
|
|
2066
|
-
return source ? Effect.succeed(source) : Effect.fail(new DocumentNotFound({ documentId }));
|
|
2067
|
-
}
|
|
2068
|
-
function requireVersion(source, expectedVersion) {
|
|
2069
|
-
return source.version === expectedVersion ? Effect.void : Effect.fail(new DocumentVersionConflict({
|
|
2070
|
-
actualVersion: source.version,
|
|
2071
|
-
documentId: source.documentId,
|
|
2072
|
-
expectedVersion
|
|
1318
|
+
const handler = AuthenticationHandler.of({ handle: rejectExternalAdminRoutes(auth.handler, basePath) });
|
|
1319
|
+
return Context.empty().pipe(Context.add(Authentication, identity.authentication), Context.add(IdentityAdministration, identity.administration), Context.add(IdentityRecovery, recovery.users), Context.add(InitialIdentity, initialization), Context.add(InitialIdentityRecovery, recovery.initial), Context.add(AuthenticationHandler, handler));
|
|
2073
1320
|
}));
|
|
2074
1321
|
}
|
|
2075
|
-
function ensurePathAvailable(state, sourcePath, exceptDocumentId) {
|
|
2076
|
-
return state.snapshot.active.some((source) => source.sourcePath === sourcePath && source.documentId !== exceptDocumentId) ? Effect.fail(new DocumentPathConflict({ sourcePath })) : Effect.void;
|
|
2077
|
-
}
|
|
2078
|
-
function replaceSource(sources, source) {
|
|
2079
|
-
return [...sources.filter((candidate) => candidate.documentId !== source.documentId), source];
|
|
2080
|
-
}
|
|
2081
|
-
function withoutDocument(sources, documentId) {
|
|
2082
|
-
return sources.filter((source) => source.documentId !== documentId);
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
//#endregion
|
|
2086
|
-
//#region ../server/src/internal/documents/document-media-references.ts
|
|
2087
|
-
/** Projects the durable authored/published media-reference union without reparsing documents. */
|
|
2088
|
-
function projectMediaReferences(authored, published, documentId) {
|
|
2089
|
-
const references = [...referencesFromCatalog(authored, documentId), ...referencesFromCatalog(published, documentId)];
|
|
2090
|
-
return [...new Map(references.map((reference) => [referenceIdentity(reference), reference])).values()];
|
|
2091
|
-
}
|
|
2092
|
-
function referencesFromCatalog(catalog, documentId) {
|
|
2093
|
-
return catalog.references.all.filter((reference) => reference.target.kind === "media" && (documentId === void 0 || reference.source.documentId === documentId)).map(toMediaReference);
|
|
2094
|
-
}
|
|
2095
|
-
function toMediaReference(reference) {
|
|
2096
|
-
if (reference.target.kind !== "media") throw new Error("Expected a media reference.");
|
|
2097
|
-
return MediaReference.make({
|
|
2098
|
-
documentId: reference.source.documentId,
|
|
2099
|
-
nodePath: reference.nodePath,
|
|
2100
|
-
objectKey: MediaObjectKey.make(reference.target.key),
|
|
2101
|
-
path: reference.source.path,
|
|
2102
|
-
sourcePath: reference.source.sourcePath
|
|
2103
|
-
});
|
|
2104
|
-
}
|
|
2105
|
-
function referenceIdentity(reference) {
|
|
2106
|
-
return `${reference.documentId}:${reference.sourcePath}:${reference.nodePath.join(".")}:${reference.objectKey}`;
|
|
2107
|
-
}
|
|
2108
1322
|
|
|
2109
1323
|
//#endregion
|
|
2110
|
-
//#region ../
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
report.created += 1;
|
|
2131
|
-
if (plan.publish) {
|
|
2132
|
-
yield* publish(dependencies, created, input, activity);
|
|
2133
|
-
report.published += 1;
|
|
2134
|
-
}
|
|
2135
|
-
break;
|
|
2136
|
-
}
|
|
2137
|
-
case "preserve":
|
|
2138
|
-
case "unchanged":
|
|
2139
|
-
report[plan.kind === "preserve" ? "preserved" : "unchanged"] += 1;
|
|
2140
|
-
break;
|
|
2141
|
-
case "publish":
|
|
2142
|
-
yield* publish(dependencies, plan.current, input, activity);
|
|
2143
|
-
report.published += 1;
|
|
2144
|
-
break;
|
|
2145
|
-
case "update": {
|
|
2146
|
-
const updated = yield* dependencies.runMutation({
|
|
2147
|
-
action: "document.update",
|
|
2148
|
-
input: {
|
|
2149
|
-
documentId: plan.current.documentId,
|
|
2150
|
-
expectedVersion: plan.current.version,
|
|
2151
|
-
markdown: source.markdown,
|
|
2152
|
-
reason: input.reason
|
|
1324
|
+
//#region ../platform-node/src/internal/http/form-ingress.ts
|
|
1325
|
+
function handleFormRequest(request, handle) {
|
|
1326
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
1327
|
+
const ingress = yield* Effect.gen(function* () {
|
|
1328
|
+
const origin = request.headers.get("origin");
|
|
1329
|
+
if (origin && origin !== new URL(request.url).origin) return Response.json({ error: "Origin rejected." }, { status: 403 });
|
|
1330
|
+
if (request.method !== "POST") return new Response(null, { status: 405 });
|
|
1331
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response(null, { status: 415 });
|
|
1332
|
+
const reader = request.body?.getReader();
|
|
1333
|
+
if (!reader) return new Response(null, { status: 400 });
|
|
1334
|
+
const chunks = [];
|
|
1335
|
+
let size = 0;
|
|
1336
|
+
try {
|
|
1337
|
+
while (true) {
|
|
1338
|
+
const chunk = yield* Effect.tryPromise(() => reader.read());
|
|
1339
|
+
if (chunk.done) break;
|
|
1340
|
+
size += chunk.value.byteLength;
|
|
1341
|
+
if (size > 131072) {
|
|
1342
|
+
yield* Effect.tryPromise(() => reader.cancel()).pipe(Effect.ignore);
|
|
1343
|
+
return new Response(null, { status: 413 });
|
|
2153
1344
|
}
|
|
2154
|
-
|
|
2155
|
-
report.updated += 1;
|
|
2156
|
-
if (plan.publish) {
|
|
2157
|
-
yield* publish(dependencies, updated, input, activity);
|
|
2158
|
-
report.published += 1;
|
|
1345
|
+
chunks.push(chunk.value);
|
|
2159
1346
|
}
|
|
2160
|
-
|
|
1347
|
+
} finally {
|
|
1348
|
+
reader.releaseLock();
|
|
2161
1349
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
};
|
|
2181
|
-
return { kind: "unchanged" };
|
|
2182
|
-
}
|
|
2183
|
-
function publish(dependencies, source, input, activity) {
|
|
2184
|
-
return dependencies.runMutation({
|
|
2185
|
-
action: "document.publish",
|
|
2186
|
-
input: {
|
|
2187
|
-
documentId: source.documentId,
|
|
2188
|
-
expectedVersion: source.version,
|
|
2189
|
-
reason: input.reason
|
|
2190
|
-
}
|
|
2191
|
-
}, activity, input.reason);
|
|
1350
|
+
const body = new Uint8Array(size);
|
|
1351
|
+
let offset = 0;
|
|
1352
|
+
for (const chunk of chunks) {
|
|
1353
|
+
body.set(chunk, offset);
|
|
1354
|
+
offset += chunk.byteLength;
|
|
1355
|
+
}
|
|
1356
|
+
return body;
|
|
1357
|
+
}).pipe(Effect.orElseSucceed(() => new Response(null, { status: 400 })));
|
|
1358
|
+
if (ingress instanceof Response) return ingress;
|
|
1359
|
+
return yield* Effect.promise(() => handle(new Request(request.url, {
|
|
1360
|
+
method: "POST",
|
|
1361
|
+
headers: request.headers,
|
|
1362
|
+
body: ingress,
|
|
1363
|
+
signal: request.signal
|
|
1364
|
+
})));
|
|
1365
|
+
}).pipe(Effect.map((response) => {
|
|
1366
|
+
response.headers.set("cache-control", "no-store");
|
|
1367
|
+
return response;
|
|
1368
|
+
})));
|
|
2192
1369
|
}
|
|
2193
1370
|
|
|
2194
1371
|
//#endregion
|
|
2195
|
-
//#region ../server/src/
|
|
2196
|
-
var
|
|
1372
|
+
//#region ../server/src/Readiness.ts
|
|
1373
|
+
var ReadinessFailed = class extends Schema.TaggedError()("Cms.ReadinessFailed", { cause: Schema.Defect() }) {};
|
|
1374
|
+
var Readiness = class extends Context.Service()("@internal/server/Readiness") {};
|
|
1375
|
+
/** A stale unresolved external change makes the installation unavailable until reconciliation resolves it. */
|
|
1376
|
+
const readinessLayer = Layer.effect(Readiness, Effect.gen(function* () {
|
|
1377
|
+
const journal = yield* ChangeJournal;
|
|
1378
|
+
const check = Effect.gen(function* () {
|
|
1379
|
+
const now = yield* DateTime.now;
|
|
1380
|
+
const pending = yield* journal.pending({
|
|
1381
|
+
before: DateTime.subtractDuration(now, "30 seconds"),
|
|
1382
|
+
limit: 1
|
|
1383
|
+
}).pipe(Effect.mapError((cause) => new ReadinessFailed({ cause })));
|
|
1384
|
+
const oldest = pending.changes[0];
|
|
1385
|
+
if (pending.total === 0) return;
|
|
1386
|
+
if (oldest === void 0) return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("The pending change journal returned no change.") });
|
|
1387
|
+
yield* Effect.logWarning("CMS readiness found a stale pending change.", {
|
|
1388
|
+
action: oldest.action,
|
|
1389
|
+
ageMillis: DateTime.toEpochMillis(now) - DateTime.toEpochMillis(oldest.updatedAt),
|
|
1390
|
+
changeId: oldest.id,
|
|
1391
|
+
phase: "pending"
|
|
1392
|
+
});
|
|
1393
|
+
return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("A stale change remains pending.") });
|
|
1394
|
+
}).pipe(Effect.withSpan("Installation.readiness"));
|
|
1395
|
+
return Readiness.of({ check });
|
|
1396
|
+
}));
|
|
2197
1397
|
|
|
2198
1398
|
//#endregion
|
|
2199
|
-
//#region ../server/src/internal/documents/document-
|
|
2200
|
-
|
|
1399
|
+
//#region ../server/src/internal/working-documents/working-document-model.ts
|
|
1400
|
+
const WorkingDocumentGenerationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("WorkingDocumentGenerationId"));
|
|
1401
|
+
const WorkingDocumentUpdateSequence = Schema.Int.check(Schema.isGreaterThan(0)).pipe(Schema.brand("WorkingDocumentUpdateSequence"));
|
|
1402
|
+
const WorkingDocumentHighWatermark = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("WorkingDocumentHighWatermark"));
|
|
1403
|
+
var WorkingDocumentPersistenceError = class extends Schema.TaggedError()("WorkingDocuments.PersistenceError", {
|
|
2201
1404
|
operation: Schema.NonEmptyString,
|
|
2202
1405
|
cause: Schema.Defect()
|
|
2203
1406
|
}) {};
|
|
2204
|
-
var
|
|
2205
|
-
|
|
2206
|
-
|
|
1407
|
+
var WorkingDocumentSuperseded = class extends Schema.TaggedError()("WorkingDocuments.Superseded", {
|
|
1408
|
+
documentId: DocumentId,
|
|
1409
|
+
expectedGeneration: WorkingDocumentGenerationId,
|
|
1410
|
+
actualGeneration: Schema.optionalKey(WorkingDocumentGenerationId)
|
|
1411
|
+
}) {};
|
|
1412
|
+
var WorkingDocumentConflict = class extends Schema.TaggedError()("WorkingDocuments.Conflict", {
|
|
1413
|
+
documentId: DocumentId,
|
|
1414
|
+
operation: Schema.NonEmptyString,
|
|
1415
|
+
message: Schema.NonEmptyString
|
|
2207
1416
|
}) {};
|
|
2208
1417
|
|
|
2209
1418
|
//#endregion
|
|
2210
|
-
//#region ../server/src/internal/documents/document
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
});
|
|
2215
|
-
const CursorJson$1 = Schema.fromJsonString(CursorPayload$1);
|
|
2216
|
-
const cursors$1 = cursorCodec(CursorJson$1, (value) => DocumentHistoryCursor.make(value), () => new DocumentHistoryInvalidCursor());
|
|
2217
|
-
function documentHistoryCapability(accessControl, repository) {
|
|
2218
|
-
const requireHistory = (context, documentId) => accessControl.require(context, {
|
|
2219
|
-
action: "document.history",
|
|
2220
|
-
resource: {
|
|
2221
|
-
documentId,
|
|
2222
|
-
kind: "document"
|
|
2223
|
-
}
|
|
2224
|
-
});
|
|
2225
|
-
const read = Effect.fn("Documents.history.read")(function* (context, documentId, revisionId) {
|
|
2226
|
-
yield* requireHistory(context, documentId);
|
|
2227
|
-
return yield* repository.revision(documentId, revisionId);
|
|
2228
|
-
});
|
|
2229
|
-
const list = Effect.fn("Documents.history.list")(function* (context, query) {
|
|
2230
|
-
yield* requireHistory(context, query.documentId);
|
|
2231
|
-
const cursor = query.cursor ? yield* Effect.fromResult(cursors$1.decode(query.cursor)) : void 0;
|
|
2232
|
-
const page = yield* repository.revisions({
|
|
2233
|
-
cursor,
|
|
2234
|
-
documentId: query.documentId,
|
|
2235
|
-
limit: query.limit ?? 25
|
|
2236
|
-
});
|
|
2237
|
-
const output = {
|
|
2238
|
-
revisions: page.revisions.map(({ markdown: _markdown, ...revision }) => revision),
|
|
2239
|
-
total: page.total
|
|
2240
|
-
};
|
|
2241
|
-
return page.next ? DocumentRevisionPage.make({
|
|
2242
|
-
...output,
|
|
2243
|
-
nextCursor: cursors$1.encode(page.next)
|
|
2244
|
-
}) : DocumentRevisionPage.make(output);
|
|
2245
|
-
});
|
|
2246
|
-
return {
|
|
2247
|
-
compare: Effect.fn("Documents.history.compare")(function* (context, input) {
|
|
2248
|
-
yield* requireHistory(context, input.documentId);
|
|
2249
|
-
const from = yield* repository.revision(input.documentId, input.fromRevisionId);
|
|
2250
|
-
const to = yield* repository.revision(input.documentId, input.toRevisionId);
|
|
2251
|
-
const changes = yield* compareLines(from.markdown, to.markdown);
|
|
2252
|
-
return DocumentRevisionComparison.make({
|
|
2253
|
-
changes,
|
|
2254
|
-
from,
|
|
2255
|
-
to
|
|
2256
|
-
});
|
|
2257
|
-
}),
|
|
2258
|
-
list,
|
|
2259
|
-
read
|
|
2260
|
-
};
|
|
2261
|
-
}
|
|
2262
|
-
const MAXIMUM_COMPARISON_LINES = 1e4;
|
|
2263
|
-
const MAXIMUM_EDIT_LENGTH = 2e3;
|
|
2264
|
-
const comparisonTooLarge = () => new DocumentHistoryComparisonTooLarge({
|
|
2265
|
-
maximumEditLength: MAXIMUM_EDIT_LENGTH,
|
|
2266
|
-
maximumLines: MAXIMUM_COMPARISON_LINES
|
|
2267
|
-
});
|
|
2268
|
-
const compareLines = Effect.fn("Documents.history.compareLines")(function* (beforeMarkdown, afterMarkdown) {
|
|
2269
|
-
const before = beforeMarkdown.split("\n");
|
|
2270
|
-
const after = afterMarkdown.split("\n");
|
|
2271
|
-
if (before.length > MAXIMUM_COMPARISON_LINES || after.length > MAXIMUM_COMPARISON_LINES) return yield* comparisonTooLarge();
|
|
2272
|
-
const diff = diffArrays(before, after, {
|
|
2273
|
-
maxEditLength: MAXIMUM_EDIT_LENGTH,
|
|
2274
|
-
oneChangePerToken: true,
|
|
2275
|
-
timeout: 100
|
|
2276
|
-
});
|
|
2277
|
-
if (!diff) return yield* comparisonTooLarge();
|
|
2278
|
-
const changes = [];
|
|
2279
|
-
let beforeLine = 1;
|
|
2280
|
-
let afterLine = 1;
|
|
2281
|
-
for (const change of diff) for (const line of change.value) if (change.removed) {
|
|
2282
|
-
changes.push({
|
|
2283
|
-
beforeLine,
|
|
2284
|
-
kind: "removed",
|
|
2285
|
-
line
|
|
2286
|
-
});
|
|
2287
|
-
beforeLine += 1;
|
|
2288
|
-
} else if (change.added) {
|
|
2289
|
-
changes.push({
|
|
2290
|
-
afterLine,
|
|
2291
|
-
kind: "added",
|
|
2292
|
-
line
|
|
2293
|
-
});
|
|
2294
|
-
afterLine += 1;
|
|
2295
|
-
} else {
|
|
2296
|
-
changes.push({
|
|
2297
|
-
afterLine,
|
|
2298
|
-
beforeLine,
|
|
2299
|
-
kind: "equal",
|
|
2300
|
-
line
|
|
2301
|
-
});
|
|
2302
|
-
beforeLine += 1;
|
|
2303
|
-
afterLine += 1;
|
|
2304
|
-
}
|
|
2305
|
-
return changes;
|
|
1419
|
+
//#region ../server/src/internal/working-documents/working-document.ts
|
|
1420
|
+
const WorkingDocumentRef = Schema.Struct({
|
|
1421
|
+
documentId: DocumentId,
|
|
1422
|
+
generationId: WorkingDocumentGenerationId
|
|
2306
1423
|
});
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
if (Option.isNone(document)) return yield* new PublishedDocumentNotFound({ lookup });
|
|
2328
|
-
const surroundQuery = Object.assign({}, { collection: input.collection ?? document.value.collection }, input.locale === void 0 && document.value.locale === void 0 ? void 0 : { locale: input.locale ?? document.value.locale }, input.where === void 0 ? void 0 : { where: input.where }, input.sort === void 0 ? void 0 : { sort: input.sort });
|
|
2329
|
-
const page = queryDocuments(current.documents, surroundQuery);
|
|
2330
|
-
const index = page.items.findIndex((candidate) => candidate.id === document.value.id);
|
|
2331
|
-
return DocumentSurround.make({
|
|
2332
|
-
previous: index > 0 ? page.items[index - 1] ?? null : null,
|
|
2333
|
-
next: index >= 0 ? page.items[index + 1] ?? null : null
|
|
2334
|
-
});
|
|
2335
|
-
});
|
|
2336
|
-
return {
|
|
2337
|
-
diagnostics: catalog.pipe(Effect.map((current) => current.issues)),
|
|
2338
|
-
navigation,
|
|
2339
|
-
query,
|
|
2340
|
-
read,
|
|
2341
|
-
surround
|
|
2342
|
-
};
|
|
2343
|
-
}
|
|
2344
|
-
function findDocument(catalog, lookup) {
|
|
2345
|
-
switch (lookup.kind) {
|
|
2346
|
-
case "id": return Option.fromNullishOr(catalog.documents.find((document) => document.id === lookup.documentId));
|
|
2347
|
-
case "path": return catalog.findByPath(lookup.path);
|
|
2348
|
-
case "key": return catalog.findByKey(lookup.key, lookup.locale).pipe(Option.filter((document) => lookup.collection === void 0 || document.collection === lookup.collection));
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
function queryDocuments(documents, query, page) {
|
|
2352
|
-
const filtered = documents.filter((document) => {
|
|
2353
|
-
if (query.collection !== void 0 && document.collection !== query.collection) return false;
|
|
2354
|
-
if (query.locale !== void 0 && document.locale !== query.locale) return false;
|
|
2355
|
-
return Object.entries(query.where ?? {}).every(([field, expected]) => matchesValue(documentField(document, field), expected));
|
|
2356
|
-
});
|
|
2357
|
-
const sorted = query.sort && query.sort.length > 0 ? sortDocuments(filtered, query.sort) : filtered;
|
|
2358
|
-
const items = page ? sorted.slice(page.offset, page.offset + page.limit) : sorted;
|
|
2359
|
-
return PublishedDocumentPage.make({
|
|
2360
|
-
items,
|
|
2361
|
-
total: filtered.length
|
|
2362
|
-
});
|
|
2363
|
-
}
|
|
2364
|
-
function createNavigation(documents, query, configuration) {
|
|
2365
|
-
const locale = query.locale ?? configuration.locales?.defaultLocale;
|
|
2366
|
-
const candidates = documents.filter((document) => (query.collection === void 0 || document.collection === query.collection) && (locale === void 0 || document.locale === locale) && document.data.navigation !== false);
|
|
2367
|
-
const roots = [];
|
|
2368
|
-
const itemsByKey = /* @__PURE__ */ new Map();
|
|
2369
|
-
const orderByItem = /* @__PURE__ */ new Map();
|
|
2370
|
-
for (const document of candidates) {
|
|
2371
|
-
if (document.key === "/") {
|
|
2372
|
-
const item = navigationItem(document);
|
|
2373
|
-
roots.push(item);
|
|
2374
|
-
orderByItem.set(item, Number.NEGATIVE_INFINITY);
|
|
2375
|
-
continue;
|
|
2376
|
-
}
|
|
2377
|
-
const segments = document.key.slice(1).split("/");
|
|
2378
|
-
let parent;
|
|
2379
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
2380
|
-
const key = PublicPath.make(`/${segments.slice(0, index + 1).join("/")}`);
|
|
2381
|
-
let item = itemsByKey.get(key);
|
|
2382
|
-
if (!item) {
|
|
2383
|
-
item = {
|
|
2384
|
-
children: [],
|
|
2385
|
-
key,
|
|
2386
|
-
title: titleFromSegment(segments[index] ?? "")
|
|
2387
|
-
};
|
|
2388
|
-
itemsByKey.set(key, item);
|
|
2389
|
-
if (parent) parent.children.push(item);
|
|
2390
|
-
else roots.push(item);
|
|
2391
|
-
}
|
|
2392
|
-
if (index === segments.length - 1) {
|
|
2393
|
-
const resolved = navigationItem(document);
|
|
2394
|
-
item.title = resolved.title;
|
|
2395
|
-
item.path = resolved.path;
|
|
2396
|
-
if (resolved.locale !== void 0) item.locale = resolved.locale;
|
|
2397
|
-
else delete item.locale;
|
|
2398
|
-
item.documentId = resolved.documentId;
|
|
2399
|
-
orderByItem.set(item, navigationOrder(document));
|
|
2400
|
-
}
|
|
2401
|
-
parent = item;
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
sortNavigation(roots, orderByItem);
|
|
2405
|
-
return roots.map((item) => DocumentNavigationItem.make(item));
|
|
2406
|
-
}
|
|
2407
|
-
function navigationItem(document) {
|
|
2408
|
-
const navigation = document.data.navigation;
|
|
2409
|
-
const configuredTitle = Schema.is(Schema.JsonObject)(navigation) ? navigation.title : void 0;
|
|
2410
|
-
return Object.assign({
|
|
2411
|
-
children: [],
|
|
2412
|
-
documentId: document.id,
|
|
2413
|
-
key: document.key,
|
|
2414
|
-
path: document.path,
|
|
2415
|
-
title: Schema.is(Schema.String)(configuredTitle) && configuredTitle.trim() ? configuredTitle.trim() : document.title
|
|
2416
|
-
}, document.locale === void 0 ? void 0 : { locale: document.locale });
|
|
2417
|
-
}
|
|
2418
|
-
function navigationOrder(document) {
|
|
2419
|
-
const navigation = document.data.navigation;
|
|
2420
|
-
const order = (Schema.is(Schema.JsonObject)(navigation) ? navigation.order : void 0) ?? document.data.order;
|
|
2421
|
-
return Schema.is(Schema.Finite)(order) ? order : Number.POSITIVE_INFINITY;
|
|
2422
|
-
}
|
|
2423
|
-
function sortNavigation(items, orderByItem) {
|
|
2424
|
-
items.sort((left, right) => {
|
|
2425
|
-
return (orderByItem.get(left) ?? Number.POSITIVE_INFINITY) - (orderByItem.get(right) ?? Number.POSITIVE_INFINITY) || left.title.localeCompare(right.title);
|
|
2426
|
-
});
|
|
2427
|
-
items.forEach((item) => sortNavigation(item.children, orderByItem));
|
|
2428
|
-
}
|
|
2429
|
-
function documentField(document, field) {
|
|
2430
|
-
switch (field) {
|
|
2431
|
-
case "collection": return document.collection;
|
|
2432
|
-
case "description": return document.description;
|
|
2433
|
-
case "id": return document.id;
|
|
2434
|
-
case "key": return document.key;
|
|
2435
|
-
case "locale": return document.locale;
|
|
2436
|
-
case "path": return document.path;
|
|
2437
|
-
case "source.path": return document.source.path;
|
|
2438
|
-
case "title": return document.title;
|
|
2439
|
-
default: return nestedValue(document.data, field.startsWith("data.") ? field.slice(5) : field);
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
function nestedValue(value, path) {
|
|
2443
|
-
return path.split(".").reduce((current, segment) => {
|
|
2444
|
-
if (!Schema.is(Schema.JsonObject)(current)) return void 0;
|
|
2445
|
-
return current[segment];
|
|
2446
|
-
}, value);
|
|
2447
|
-
}
|
|
2448
|
-
function matchesValue(actual, expected) {
|
|
2449
|
-
const expectedValues = Array.isArray(expected) ? expected : [expected];
|
|
2450
|
-
return (Array.isArray(actual) ? actual : [actual]).some((value) => Schema.is(DocumentQueryScalar)(value) && expectedValues.includes(value));
|
|
2451
|
-
}
|
|
2452
|
-
function sortDocuments(documents, sorts) {
|
|
2453
|
-
return documents.toSorted((left, right) => {
|
|
2454
|
-
for (const sort of sorts) {
|
|
2455
|
-
const comparison = compareValues(documentField(left, sort.field), documentField(right, sort.field));
|
|
2456
|
-
if (comparison !== 0) return sort.direction === "desc" ? -comparison : comparison;
|
|
2457
|
-
}
|
|
2458
|
-
return 0;
|
|
2459
|
-
});
|
|
2460
|
-
}
|
|
2461
|
-
function compareValues(left, right) {
|
|
2462
|
-
if (left === right) return 0;
|
|
2463
|
-
if (left === void 0 || left === null) return 1;
|
|
2464
|
-
if (right === void 0 || right === null) return -1;
|
|
2465
|
-
if (Schema.is(Schema.Finite)(left) && Schema.is(Schema.Finite)(right)) return left - right;
|
|
2466
|
-
const leftText = Schema.is(Schema.String)(left) ? left : JSON.stringify(left) ?? "";
|
|
2467
|
-
const rightText = Schema.is(Schema.String)(right) ? right : JSON.stringify(right) ?? "";
|
|
2468
|
-
return leftText.localeCompare(rightText);
|
|
2469
|
-
}
|
|
2470
|
-
function titleFromSegment(segment) {
|
|
2471
|
-
return segment.replace(/[-_]+/gu, " ").replace(/^./u, (character) => character.toUpperCase());
|
|
2472
|
-
}
|
|
2473
|
-
|
|
2474
|
-
//#endregion
|
|
2475
|
-
//#region ../server/src/internal/documents/documents.ts
|
|
2476
|
-
const isDocumentGenerationConflict$2 = Schema.is(DocumentGenerationConflict);
|
|
2477
|
-
const isDocumentMediaUnavailable$1 = Schema.is(DocumentMediaUnavailable);
|
|
2478
|
-
const CONTENT_MUTATION_BUFFER_CAPACITY = 16;
|
|
2479
|
-
var Documents = class extends Context.Service()("@internal/server/Documents") {};
|
|
2480
|
-
var DocumentPreviews = class extends Context.Service()("@internal/server/DocumentPreviews") {};
|
|
2481
|
-
var PublishedDocuments = class extends Context.Service()("@internal/server/PublishedDocuments") {};
|
|
2482
|
-
var DocumentMaintenance = class extends Context.Service()("@internal/server/internal/DocumentMaintenance") {};
|
|
2483
|
-
const makeDocuments = Effect.fn("Documents.make")(function* (configuration, options) {
|
|
2484
|
-
const repository = yield* DocumentStore;
|
|
2485
|
-
const accessControl = yield* AccessControl;
|
|
2486
|
-
const crypto = yield* Crypto.Crypto;
|
|
2487
|
-
const engine = yield* createDocumentEngine(configuration, options);
|
|
2488
|
-
const state = yield* Ref.make(yield* loadSynchronizedState(repository, engine));
|
|
2489
|
-
const semaphore = yield* Semaphore.make(1);
|
|
2490
|
-
const mutationEvents = yield* Effect.acquireRelease(PubSub.sliding(CONTENT_MUTATION_BUFFER_CAPACITY), PubSub.shutdown);
|
|
2491
|
-
const dependencies = {
|
|
2492
|
-
engine,
|
|
2493
|
-
configuration,
|
|
2494
|
-
repository,
|
|
2495
|
-
components: options.components
|
|
2496
|
-
};
|
|
2497
|
-
const refreshUnlocked = Effect.fn("Documents.refreshUnlocked")(function* (force = false) {
|
|
2498
|
-
const current = yield* Ref.get(state);
|
|
2499
|
-
const snapshot = force ? yield* loadRequired(repository) : yield* repository.load(current.snapshot.generation);
|
|
2500
|
-
if (!snapshot || !generationIsAfter(snapshot.generation, current.snapshot.generation)) return current.snapshot.generation;
|
|
2501
|
-
const refreshed = yield* compileDocumentState(engine, snapshot, current);
|
|
2502
|
-
yield* Ref.set(state, refreshed);
|
|
2503
|
-
return refreshed.snapshot.generation;
|
|
2504
|
-
});
|
|
2505
|
-
const executeUnlocked = Effect.fn("Documents.execute")(function* (command, activity) {
|
|
2506
|
-
return yield* Effect.gen(function* () {
|
|
2507
|
-
yield* refreshUnlocked();
|
|
2508
|
-
const current = yield* Ref.get(state);
|
|
2509
|
-
if (command.action === "document.move" || command.action === "document.remove" || command.action === "document.duplicate") {
|
|
2510
|
-
const ids = command.action === "document.duplicate" && !command.input.includeChildren ? [command.input.documentId] : subtreeIds(current, command.input.documentId);
|
|
2511
|
-
for (const documentId of ids) yield* accessControl.require({
|
|
2512
|
-
actor: activity.actor,
|
|
2513
|
-
requestId: activity.requestId
|
|
2514
|
-
}, {
|
|
2515
|
-
action: command.action,
|
|
2516
|
-
resource: {
|
|
2517
|
-
kind: "document",
|
|
2518
|
-
documentId
|
|
2519
|
-
}
|
|
2520
|
-
});
|
|
2521
|
-
if (command.action === "document.duplicate") {
|
|
2522
|
-
const source = current.authoredById.get(command.input.documentId);
|
|
2523
|
-
if (source) yield* accessControl.require({
|
|
2524
|
-
actor: activity.actor,
|
|
2525
|
-
requestId: activity.requestId
|
|
2526
|
-
}, {
|
|
2527
|
-
action: "document.create",
|
|
2528
|
-
resource: {
|
|
2529
|
-
kind: "collection",
|
|
2530
|
-
collection: source.collection,
|
|
2531
|
-
locale: source.locale
|
|
2532
|
-
}
|
|
2533
|
-
});
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
const prepared = yield* prepareDocumentMutation(dependencies, current, command);
|
|
2537
|
-
if (command.action === "document.move" && prepared._tag === "Commit") {
|
|
2538
|
-
for (const plan of prepared.additionalPlans ?? []) if (plan.action === "document.update") yield* accessControl.require({
|
|
2539
|
-
actor: activity.actor,
|
|
2540
|
-
requestId: activity.requestId
|
|
2541
|
-
}, {
|
|
2542
|
-
action: "document.update",
|
|
2543
|
-
resource: {
|
|
2544
|
-
kind: "document",
|
|
2545
|
-
documentId: plan.documentId
|
|
2546
|
-
}
|
|
2547
|
-
});
|
|
2548
|
-
}
|
|
2549
|
-
if (command.action === "document.undelete" && prepared._tag === "Commit") for (const plan of [prepared.commitPlan, ...prepared.additionalPlans ?? []]) yield* accessControl.require({
|
|
2550
|
-
actor: activity.actor,
|
|
2551
|
-
requestId: activity.requestId
|
|
2552
|
-
}, {
|
|
2553
|
-
action: "document.undelete",
|
|
2554
|
-
resource: {
|
|
2555
|
-
kind: "document",
|
|
2556
|
-
documentId: plan.documentId
|
|
2557
|
-
}
|
|
2558
|
-
});
|
|
2559
|
-
if (prepared._tag === "Unchanged") return prepared.result;
|
|
2560
|
-
const committed = yield* repository.commit({
|
|
2561
|
-
activity,
|
|
2562
|
-
plan: prepared.commitPlan,
|
|
2563
|
-
additionalPlans: prepared.additionalPlans,
|
|
2564
|
-
expectedGeneration: current.snapshot.generation,
|
|
2565
|
-
mediaReferences: [prepared.commitPlan, ...prepared.additionalPlans ?? []].flatMap((plan) => projectMediaReferences(prepared.authored, prepared.published, plan.documentId))
|
|
2566
|
-
});
|
|
2567
|
-
const next = makeDocumentState(prepared.authored, prepared.published, committed.snapshot);
|
|
2568
|
-
yield* Ref.set(state, next);
|
|
2569
|
-
const entry = entryById(next, committed.mutation.documentId);
|
|
2570
|
-
const result = DocumentMutationResult.make(Object.assign({}, committed.mutation, entry === void 0 ? void 0 : { entry }));
|
|
2571
|
-
yield* PubSub.publish(mutationEvents, result);
|
|
2572
|
-
return result;
|
|
2573
|
-
}).pipe(Effect.tapError((error) => isDocumentGenerationConflict$2(error) ? refreshUnlocked(true) : Effect.void), Effect.retry({
|
|
2574
|
-
schedule: Schedule.recurs(3),
|
|
2575
|
-
while: isDocumentGenerationConflict$2
|
|
2576
|
-
}));
|
|
2577
|
-
});
|
|
2578
|
-
const runMutation = Effect.fn("Documents.runMutation")(function* (command, activity, reason) {
|
|
2579
|
-
const resolvedActivity = enrichActivity(activity, reason);
|
|
2580
|
-
return yield* semaphore.withPermits(1)(executeUnlocked(command, resolvedActivity));
|
|
2581
|
-
});
|
|
2582
|
-
const authored = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2583
|
-
yield* refreshUnlocked();
|
|
2584
|
-
return (yield* Ref.get(state)).authored;
|
|
2585
|
-
})).pipe(Effect.withSpan("Documents.authored"));
|
|
2586
|
-
const browse = Effect.fn("Documents.browse")((query) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2587
|
-
yield* refreshUnlocked();
|
|
2588
|
-
return authoringEntries(yield* Ref.get(state), query);
|
|
2589
|
-
})));
|
|
2590
|
-
const create = Effect.fn("Documents.create")(function* (input, activity) {
|
|
2591
|
-
const plan = yield* Effect.fromResult(planDocumentCreation(configuration, input));
|
|
2592
|
-
const source = yield* createInitialDocumentSource(plan);
|
|
2593
|
-
const documentId = DocumentId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
2594
|
-
return yield* runMutation({
|
|
2595
|
-
action: "document.create",
|
|
2596
|
-
documentId,
|
|
2597
|
-
markdown: source.markdown,
|
|
2598
|
-
sourcePath: plan.sourcePath
|
|
2599
|
-
}, activity, input.reason);
|
|
2600
|
-
});
|
|
2601
|
-
const importSources = (input, activity) => importDocumentSources({
|
|
2602
|
-
current: (sourcePath) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2603
|
-
yield* refreshUnlocked();
|
|
2604
|
-
return (yield* Ref.get(state)).snapshot.active.find((candidate) => candidate.sourcePath === sourcePath);
|
|
2605
|
-
})),
|
|
2606
|
-
nextDocumentId: crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((value) => DocumentId.make(value))),
|
|
2607
|
-
runMutation
|
|
2608
|
-
}, input, activity);
|
|
2609
|
-
const duplicate = Effect.fn("Documents.duplicate")((input, activity) => runMutation({
|
|
2610
|
-
action: "document.duplicate",
|
|
2611
|
-
input,
|
|
2612
|
-
nextDocumentId: crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((value) => DocumentId.make(value)))
|
|
2613
|
-
}, activity, input.reason));
|
|
2614
|
-
const unpublish = Effect.fn("Documents.unpublish")((input, activity) => runMutation({
|
|
2615
|
-
action: "document.unpublish",
|
|
2616
|
-
input
|
|
2617
|
-
}, activity, input.reason));
|
|
2618
|
-
const move = Effect.fn("Documents.move")((input, activity) => runMutation({
|
|
2619
|
-
action: "document.move",
|
|
2620
|
-
input
|
|
2621
|
-
}, activity, input.reason));
|
|
2622
|
-
const subscribeMutations = Effect.gen(function* () {
|
|
2623
|
-
const subscription = yield* PubSub.subscribe(mutationEvents);
|
|
2624
|
-
const baseline = yield* semaphore.withPermits(1)(Ref.get(state).pipe(Effect.map((current) => current.snapshot.generation)));
|
|
2625
|
-
return {
|
|
2626
|
-
baseline,
|
|
2627
|
-
mutations: Stream.fromSubscription(subscription).pipe(Stream.filter((mutation) => generationIsAfter(mutation.generation, baseline)))
|
|
2628
|
-
};
|
|
2629
|
-
}).pipe(Effect.withSpan("Documents.subscribeMutations"));
|
|
2630
|
-
const publish = Effect.fn("Documents.publish")((input, activity) => runMutation({
|
|
2631
|
-
action: "document.publish",
|
|
2632
|
-
input
|
|
2633
|
-
}, activity, input.reason));
|
|
2634
|
-
const published = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2635
|
-
yield* refreshUnlocked();
|
|
2636
|
-
return (yield* Ref.get(state)).published;
|
|
2637
|
-
})).pipe(Effect.withSpan("Documents.published"));
|
|
2638
|
-
const read = Effect.fn("Documents.read")((documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2639
|
-
yield* refreshUnlocked();
|
|
2640
|
-
return (yield* Ref.get(state)).activeById.get(documentId) || (yield* new DocumentNotFound({ documentId }));
|
|
2641
|
-
})));
|
|
2642
|
-
const refresh = semaphore.withPermits(1)(refreshUnlocked()).pipe(Effect.withSpan("Documents.refresh"));
|
|
2643
|
-
const remove = Effect.fn("Documents.remove")((input, activity) => runMutation({
|
|
2644
|
-
action: "document.remove",
|
|
2645
|
-
input
|
|
2646
|
-
}, activity, input.reason));
|
|
2647
|
-
const restore = Effect.fn("Documents.restore")((input, activity) => runMutation({
|
|
2648
|
-
action: "document.restore",
|
|
2649
|
-
input
|
|
2650
|
-
}, activity, input.reason));
|
|
2651
|
-
const trash = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2652
|
-
yield* refreshUnlocked();
|
|
2653
|
-
return deletedEntries(yield* Ref.get(state));
|
|
2654
|
-
})).pipe(Effect.withSpan("Documents.trash"));
|
|
2655
|
-
const undelete = Effect.fn("Documents.undelete")((input, activity) => runMutation({
|
|
2656
|
-
action: "document.undelete",
|
|
2657
|
-
input
|
|
2658
|
-
}, activity, input.reason));
|
|
2659
|
-
const update = Effect.fn("Documents.update")((input, activity) => runMutation({
|
|
2660
|
-
action: "document.update",
|
|
2661
|
-
input
|
|
2662
|
-
}, activity, input.reason));
|
|
2663
|
-
const validate = Effect.fn("Documents.validate")((input) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2664
|
-
yield* refreshUnlocked();
|
|
2665
|
-
return yield* validateDocumentUpdate(engine, yield* Ref.get(state), input);
|
|
2666
|
-
})));
|
|
2667
|
-
const history = documentHistoryCapability(accessControl, repository);
|
|
2668
|
-
const delivery = documentDelivery(configuration, published);
|
|
2669
|
-
const document = (documentId) => ({
|
|
2670
|
-
documentId,
|
|
2671
|
-
kind: "document"
|
|
2672
|
-
});
|
|
2673
|
-
const requireThen = (context, action, resource, effect) => accessControl.require(context, {
|
|
2674
|
-
action,
|
|
2675
|
-
resource
|
|
2676
|
-
}).pipe(Effect.andThen(effect));
|
|
2677
|
-
const documents = Documents.of({
|
|
2678
|
-
browse: (context, query) => requireThen(context, "document.browse", query.collection ? {
|
|
2679
|
-
collection: query.collection,
|
|
2680
|
-
kind: "collection",
|
|
2681
|
-
locale: query.locale
|
|
2682
|
-
} : { kind: "catalog" }, () => browse(query)),
|
|
2683
|
-
create: (context, input) => requireThen(context, "document.create", {
|
|
2684
|
-
collection: input.collection,
|
|
2685
|
-
kind: "collection",
|
|
2686
|
-
locale: input.locale
|
|
2687
|
-
}, () => create(input, requestActivity(context, input.reason))),
|
|
2688
|
-
historyCompare: history.compare,
|
|
2689
|
-
historyList: history.list,
|
|
2690
|
-
historyRead: history.read,
|
|
2691
|
-
duplicate: (context, input) => requireThen(context, "document.duplicate", document(input.documentId), () => duplicate(input, requestActivity(context, input.reason))),
|
|
2692
|
-
unpublish: (context, input) => requireThen(context, "document.unpublish", document(input.documentId), () => unpublish(input, requestActivity(context, input.reason))),
|
|
2693
|
-
move: (context, input) => requireThen(context, "document.move", document(input.documentId), () => move(input, requestActivity(context, input.reason))),
|
|
2694
|
-
publish: (context, input) => requireThen(context, "document.publish", document(input.documentId), () => publish(input, requestActivity(context, input.reason))),
|
|
2695
|
-
read: (context, documentId) => requireThen(context, "document.read", document(documentId), () => read(documentId)),
|
|
2696
|
-
remove: (context, input) => requireThen(context, "document.remove", document(input.documentId), () => remove(input, requestActivity(context, input.reason))),
|
|
2697
|
-
restore: (context, input) => requireThen(context, "document.restore", document(input.documentId), () => restore(input, requestActivity(context, input.reason))),
|
|
2698
|
-
trash: (context) => requireThen(context, "document.trash", { kind: "catalog" }, () => trash),
|
|
2699
|
-
undelete: (context, input) => requireThen(context, "document.undelete", document(input.documentId), () => undelete(input, requestActivity(context, input.reason))),
|
|
2700
|
-
update: (context, input) => requireThen(context, "document.update", document(input.documentId), () => update(input, requestActivity(context, input.reason))),
|
|
2701
|
-
validate: (context, input) => requireThen(context, "document.validate", document(input.documentId), () => validate(input))
|
|
2702
|
-
});
|
|
2703
|
-
const draftDelivery = documentDelivery(configuration, authored);
|
|
2704
|
-
const previews = DocumentPreviews.of({
|
|
2705
|
-
read: (context, lookup) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.read(lookup)),
|
|
2706
|
-
query: (context, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.query(query)),
|
|
2707
|
-
navigation: (context, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.navigation(query)),
|
|
2708
|
-
surround: (context, lookup, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.surround(lookup, query))
|
|
2709
|
-
});
|
|
2710
|
-
const publishedService = PublishedDocuments.of({
|
|
2711
|
-
navigation: delivery.navigation,
|
|
2712
|
-
query: delivery.query,
|
|
2713
|
-
read: delivery.read,
|
|
2714
|
-
surround: delivery.surround,
|
|
2715
|
-
diagnostics: (context) => accessControl.require(context, {
|
|
2716
|
-
action: "document.browse",
|
|
2717
|
-
resource: { kind: "catalog" }
|
|
2718
|
-
}).pipe(Effect.andThen(delivery.diagnostics))
|
|
2719
|
-
});
|
|
2720
|
-
return {
|
|
2721
|
-
documents,
|
|
2722
|
-
maintenance: DocumentMaintenance.of({
|
|
2723
|
-
importSources,
|
|
2724
|
-
mediaReferences: (documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2725
|
-
yield* refreshUnlocked();
|
|
2726
|
-
const current = yield* Ref.get(state);
|
|
2727
|
-
return projectMediaReferences(current.authored, current.published, documentId);
|
|
2728
|
-
})),
|
|
2729
|
-
publish,
|
|
2730
|
-
read,
|
|
2731
|
-
refresh,
|
|
2732
|
-
subscribeMutations,
|
|
2733
|
-
validate
|
|
2734
|
-
}),
|
|
2735
|
-
previews,
|
|
2736
|
-
publishedDocuments: publishedService
|
|
2737
|
-
};
|
|
1424
|
+
var WorkingDocumentInvalid = class extends Schema.TaggedError()("WorkingDocuments.Invalid", {
|
|
1425
|
+
documentId: DocumentId,
|
|
1426
|
+
message: Schema.NonEmptyString,
|
|
1427
|
+
cause: Schema.Defect()
|
|
1428
|
+
}) {};
|
|
1429
|
+
var WorkingDocumentUnavailable = class extends Schema.TaggedError()("WorkingDocuments.Unavailable", {
|
|
1430
|
+
operation: Schema.NonEmptyString,
|
|
1431
|
+
cause: Schema.Defect()
|
|
1432
|
+
}) {};
|
|
1433
|
+
/** An authored or working baseline committed, but the publish workflow still needs recovery. */
|
|
1434
|
+
var WorkingDocumentCheckpointed = class extends Schema.TaggedError()("WorkingDocuments.Checkpointed", {
|
|
1435
|
+
checkpointVersion: DocumentVersion,
|
|
1436
|
+
documentId: DocumentId,
|
|
1437
|
+
result: Schema.optionalKey(DocumentMutationResult),
|
|
1438
|
+
stateVector: Schema.Uint8Array,
|
|
1439
|
+
cause: Schema.Defect()
|
|
1440
|
+
}) {};
|
|
1441
|
+
const WorkingDocumentChange = Schema.Struct({
|
|
1442
|
+
kind: Schema.Literals(["updated", "invalidated"]),
|
|
1443
|
+
ref: WorkingDocumentRef
|
|
2738
1444
|
});
|
|
2739
|
-
|
|
2740
|
-
return Layer.effectContext(makeDocuments(configuration, options).pipe(Effect.map(({ documents, maintenance, previews, publishedDocuments }) => Context.empty().pipe(Context.add(Documents, documents), Context.add(DocumentMaintenance, maintenance), Context.add(DocumentPreviews, previews), Context.add(PublishedDocuments, publishedDocuments)))));
|
|
2741
|
-
}
|
|
2742
|
-
function loadRequired(repository) {
|
|
2743
|
-
return repository.load().pipe(Effect.filterOrFail((snapshot) => snapshot !== void 0, () => new DocumentPersistenceError({
|
|
2744
|
-
cause: /* @__PURE__ */ new Error("DocumentStore returned no initial snapshot."),
|
|
2745
|
-
operation: "DocumentStore.load"
|
|
2746
|
-
})));
|
|
2747
|
-
}
|
|
2748
|
-
function loadSynchronizedState(repository, engine) {
|
|
2749
|
-
return Effect.gen(function* () {
|
|
2750
|
-
const snapshot = yield* loadRequired(repository);
|
|
2751
|
-
const state = yield* compileDocumentState(engine, snapshot);
|
|
2752
|
-
yield* repository.synchronizeMediaReferences({
|
|
2753
|
-
expectedGeneration: snapshot.generation,
|
|
2754
|
-
references: projectMediaReferences(state.authored, state.published)
|
|
2755
|
-
});
|
|
2756
|
-
return state;
|
|
2757
|
-
}).pipe(Effect.retry({
|
|
2758
|
-
schedule: Schedule.recurs(3),
|
|
2759
|
-
while: isDocumentGenerationConflict$2
|
|
2760
|
-
}), Effect.mapError((error) => isDocumentGenerationConflict$2(error) || isDocumentMediaUnavailable$1(error) ? new DocumentPersistenceError({
|
|
2761
|
-
cause: error,
|
|
2762
|
-
operation: "Documents.synchronizeMediaReferences"
|
|
2763
|
-
}) : error));
|
|
2764
|
-
}
|
|
2765
|
-
function generationIsAfter(candidate, current) {
|
|
2766
|
-
return BigInt(candidate) > BigInt(current);
|
|
2767
|
-
}
|
|
2768
|
-
function enrichActivity(activity, reason) {
|
|
2769
|
-
return ActivityContext.make(Object.assign({}, activity, reason === void 0 ? void 0 : { reason }));
|
|
2770
|
-
}
|
|
2771
|
-
function requestActivity(context, reason) {
|
|
2772
|
-
return ActivityContext.make(Object.assign({
|
|
2773
|
-
actor: context.actor,
|
|
2774
|
-
requestId: context.requestId
|
|
2775
|
-
}, reason === void 0 ? void 0 : { reason }));
|
|
2776
|
-
}
|
|
1445
|
+
var WorkingDocuments = class extends Context.Service()("@internal/server/WorkingDocuments") {};
|
|
2777
1446
|
|
|
2778
1447
|
//#endregion
|
|
2779
1448
|
//#region ../document/src/internal/editor/identity-extension.ts
|
|
2780
|
-
/** Ensures Yjs only observes authorable nodes with final, unique identities. */
|
|
2781
1449
|
const EditorNodeIdentity = Extension.create({
|
|
2782
1450
|
name: "cmsNodeIdentity",
|
|
2783
1451
|
priority: 1e4,
|
|
@@ -3326,7 +1994,7 @@ function unwrapFirstParagraph(nodes) {
|
|
|
3326
1994
|
}
|
|
3327
1995
|
function restoreOpaqueNode(value, nodeId) {
|
|
3328
1996
|
const decoded = Schema.decodeUnknownOption(MarkdownNode)(value);
|
|
3329
|
-
if (decoded
|
|
1997
|
+
if (Option.isNone(decoded) || Predicate.isString(decoded.value)) return void 0;
|
|
3330
1998
|
const node = structuredClone(decoded.value);
|
|
3331
1999
|
if (nodeId === void 0) return node;
|
|
3332
2000
|
if (node[0] === null) return [
|
|
@@ -3436,10 +2104,10 @@ const PROPERTIES_NAME = "properties";
|
|
|
3436
2104
|
const PROPS_PREFIX = "props:";
|
|
3437
2105
|
const CMS_PROPS_PREFIX = "cmsProps:";
|
|
3438
2106
|
const editorSchema$1 = createEditorSchema();
|
|
3439
|
-
const decodeEditorNodeId = Schema.
|
|
3440
|
-
const decodeJson$1 = Schema.
|
|
3441
|
-
const decodeJsonObject$1 = Schema.
|
|
3442
|
-
const decodePath = Schema.
|
|
2107
|
+
const decodeEditorNodeId = flow(Schema.decodeUnknownResult(EditorNodeId), Result.getOrThrow);
|
|
2108
|
+
const decodeJson$1 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
|
|
2109
|
+
const decodeJsonObject$1 = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
|
|
2110
|
+
const decodePath = flow(Schema.decodeUnknownResult(Schema.Array(Schema.String)), Result.getOrThrow);
|
|
3443
2111
|
function seedCollaborationFields(document, content) {
|
|
3444
2112
|
const state = extractGranularState(content);
|
|
3445
2113
|
replaceMap(document.getMap(FRONTMATTER_NAME), flatten(state.frontmatter));
|
|
@@ -3589,9 +2257,9 @@ const METADATA_NAME = "cms";
|
|
|
3589
2257
|
const META_KEY = "meta";
|
|
3590
2258
|
const SOURCE_PATH_KEY = "sourcePath";
|
|
3591
2259
|
const editorSchema = createEditorSchema();
|
|
3592
|
-
const decodeJson = Schema.
|
|
3593
|
-
const decodeJsonObject = Schema.
|
|
3594
|
-
const decodeSourcePath = Schema.
|
|
2260
|
+
const decodeJson = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
|
|
2261
|
+
const decodeJsonObject = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
|
|
2262
|
+
const decodeSourcePath = flow(Schema.decodeUnknownResult(SourcePath), Result.getOrThrow);
|
|
3595
2263
|
function seedCollaborationDocument(sourcePath, seed, meta) {
|
|
3596
2264
|
const document = prosemirrorJSONToYDoc(editorSchema, seed, BODY_NAME);
|
|
3597
2265
|
try {
|
|
@@ -3688,7 +2356,6 @@ function sameBytes$1(left, right) {
|
|
|
3688
2356
|
|
|
3689
2357
|
//#endregion
|
|
3690
2358
|
//#region ../document/src/CollaborationDocument.ts
|
|
3691
|
-
/** Creates the initial collaborative state from the canonical editor projection. */
|
|
3692
2359
|
const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(function* (input) {
|
|
3693
2360
|
const seed = yield* openEditorDocument(input.body);
|
|
3694
2361
|
return yield* Effect.try({
|
|
@@ -3696,7 +2363,6 @@ const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(fu
|
|
|
3696
2363
|
catch: (cause) => invalidCollaboration("Could not create the collaborative editor document.", cause)
|
|
3697
2364
|
});
|
|
3698
2365
|
});
|
|
3699
|
-
/** Renders collaborative state through the canonical editor-to-Markdown encoder. */
|
|
3700
2366
|
const renderCollaborationDocument = Effect.fn("CollaborationDocument.render")(function* (document) {
|
|
3701
2367
|
const inspected = yield* inspectCollaborationDocument(document);
|
|
3702
2368
|
return {
|
|
@@ -3715,31 +2381,26 @@ const captureCollaborationDocument = Effect.fn("CollaborationDocument.capture")(
|
|
|
3715
2381
|
catch: (cause) => invalidCollaboration("Could not capture the collaborative editor document.", cause)
|
|
3716
2382
|
});
|
|
3717
2383
|
});
|
|
3718
|
-
/** Restores a validated, caller-owned document from a full collaboration snapshot. */
|
|
3719
2384
|
const restoreCollaborationDocument = Effect.fn("CollaborationDocument.restore")(function* (snapshot) {
|
|
3720
2385
|
return yield* validateRestoredDocument(yield* Effect.try({
|
|
3721
2386
|
try: () => restoreCollaborationSnapshot(snapshot),
|
|
3722
2387
|
catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
|
|
3723
2388
|
}));
|
|
3724
2389
|
});
|
|
3725
|
-
/** Restores a validated document when only its authoritative full-state update is persisted. */
|
|
3726
2390
|
const restoreCollaborationState = Effect.fn("CollaborationDocument.restoreState")(function* (state) {
|
|
3727
2391
|
return yield* validateRestoredDocument(yield* Effect.try({
|
|
3728
2392
|
try: () => restoreCollaborationUpdate(state),
|
|
3729
2393
|
catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
|
|
3730
2394
|
}));
|
|
3731
2395
|
});
|
|
3732
|
-
/** Applies one encoded collaboration update while translating corrupt data to a typed failure. */
|
|
3733
2396
|
const applyCollaborationUpdate = Effect.fn("CollaborationDocument.applyUpdate")((document, update) => Effect.try({
|
|
3734
2397
|
try: () => applyCollaborationStateUpdate(document, update),
|
|
3735
2398
|
catch: (cause) => invalidCollaboration("Could not apply the collaborative document update.", cause)
|
|
3736
2399
|
}));
|
|
3737
|
-
/** Captures the current state vector and an optional differential update. */
|
|
3738
2400
|
const synchronizeCollaborationState = Effect.fn("CollaborationDocument.synchronizeState")((document, stateVector) => Effect.try({
|
|
3739
2401
|
try: () => synchronizeCollaborationStateValue(document, stateVector),
|
|
3740
2402
|
catch: (cause) => invalidCollaboration("Could not synchronize the collaborative document state.", cause)
|
|
3741
2403
|
}));
|
|
3742
|
-
/** Releases a collaboration document owned by a scoped workflow. */
|
|
3743
2404
|
function destroyCollaborationDocument(document) {
|
|
3744
2405
|
return Effect.sync(() => document.destroy());
|
|
3745
2406
|
}
|
|
@@ -3840,6 +2501,25 @@ function sameJson(left, right) {
|
|
|
3840
2501
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
3841
2502
|
}
|
|
3842
2503
|
|
|
2504
|
+
//#endregion
|
|
2505
|
+
//#region ../server/src/internal/working-documents/working-document-source.ts
|
|
2506
|
+
const validatedWorkingDocument = Effect.fn("WorkingDocuments.validateSource")(function* (source, validate) {
|
|
2507
|
+
const validation = yield* validate(UpdateDocumentInput.make({
|
|
2508
|
+
documentId: source.documentId,
|
|
2509
|
+
expectedVersion: source.version,
|
|
2510
|
+
markdown: source.markdown
|
|
2511
|
+
}));
|
|
2512
|
+
if (!validation.valid || !validation.document) return yield* new WorkingDocumentInvalid({
|
|
2513
|
+
cause: validation.issues,
|
|
2514
|
+
documentId: source.documentId,
|
|
2515
|
+
message: "Authored document cannot initialize a Working Document."
|
|
2516
|
+
});
|
|
2517
|
+
return validation.document;
|
|
2518
|
+
});
|
|
2519
|
+
const newWorkingSnapshot = Effect.fn("WorkingDocuments.newSnapshot")(function* (source, document, generationId) {
|
|
2520
|
+
return yield* createWorkingSnapshot(source, document, yield* generationId(source.documentId));
|
|
2521
|
+
});
|
|
2522
|
+
|
|
3843
2523
|
//#endregion
|
|
3844
2524
|
//#region ../server/src/internal/working-documents/working-document-reconciliation.ts
|
|
3845
2525
|
const isDocumentNotFound$3 = Schema.is(DocumentNotFound);
|
|
@@ -3854,18 +2534,6 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
|
|
|
3854
2534
|
})
|
|
3855
2535
|
}).pipe(Effect.asVoid);
|
|
3856
2536
|
const currentSource = (documentId) => options.documents.read(documentId).pipe(Effect.asSome, Effect.catchIf(isDocumentNotFound$3, () => Effect.succeedNone));
|
|
3857
|
-
const validatedDocument = Effect.fn("WorkingDocuments.reconcile.validateSource")(function* (source) {
|
|
3858
|
-
const validation = yield* options.documents.validate(UpdateDocumentInput.make({
|
|
3859
|
-
documentId: source.documentId,
|
|
3860
|
-
expectedVersion: source.version,
|
|
3861
|
-
markdown: source.markdown
|
|
3862
|
-
}));
|
|
3863
|
-
if (!validation.valid || !validation.document) return yield* invalidSource(source.documentId, validation);
|
|
3864
|
-
return validation.document;
|
|
3865
|
-
});
|
|
3866
|
-
const newSnapshot = Effect.fn("WorkingDocuments.reconcile.newSnapshot")(function* (source, document) {
|
|
3867
|
-
return yield* createWorkingSnapshot(source, document, yield* options.generationId(source.documentId));
|
|
3868
|
-
});
|
|
3869
2537
|
const rotateDirty = Effect.fn("WorkingDocuments.reconcile.rotateDirty")(function* (snapshot) {
|
|
3870
2538
|
const generationId = yield* options.generationId(snapshot.documentId);
|
|
3871
2539
|
const seed = yield* useWorkingSnapshot(snapshot, (document) => captureCollaborationDocument(document).pipe(Effect.map((captured) => rotatedSeed(snapshot, generationId, captured))));
|
|
@@ -3901,10 +2569,10 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
|
|
|
3901
2569
|
yield* publishChange(loaded.value);
|
|
3902
2570
|
return;
|
|
3903
2571
|
}
|
|
3904
|
-
const document = yield*
|
|
2572
|
+
const document = yield* validatedWorkingDocument(source.value, options.documents.validate);
|
|
3905
2573
|
yield* options.store.replace({
|
|
3906
2574
|
expectedGeneration: loaded.value.generationId,
|
|
3907
|
-
snapshot: yield*
|
|
2575
|
+
snapshot: yield* newWorkingSnapshot(source.value, document, options.generationId)
|
|
3908
2576
|
});
|
|
3909
2577
|
yield* publishChange(loaded.value);
|
|
3910
2578
|
}));
|
|
@@ -3990,13 +2658,6 @@ function rotatedSeed(snapshot, generationId, captured) {
|
|
|
3990
2658
|
state: Uint8Array.from(captured.state)
|
|
3991
2659
|
};
|
|
3992
2660
|
}
|
|
3993
|
-
function invalidSource(documentId, validation) {
|
|
3994
|
-
return Effect.fail(new WorkingDocumentInvalid({
|
|
3995
|
-
cause: validation.issues,
|
|
3996
|
-
documentId,
|
|
3997
|
-
message: "Authored document cannot initialize a Working Document."
|
|
3998
|
-
}));
|
|
3999
|
-
}
|
|
4000
2661
|
function generationAtOrBefore(candidate, current) {
|
|
4001
2662
|
return BigInt(candidate) <= BigInt(current);
|
|
4002
2663
|
}
|
|
@@ -4004,6 +2665,28 @@ function generationsAreAdjacent(current, candidate) {
|
|
|
4004
2665
|
return BigInt(candidate) === BigInt(current) + 1n;
|
|
4005
2666
|
}
|
|
4006
2667
|
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region ../server/src/internal/working-documents/document-locks.ts
|
|
2670
|
+
/** Retain a lock only while an operation is running or waiting for that document. */
|
|
2671
|
+
function documentSynchronization() {
|
|
2672
|
+
const locks = /* @__PURE__ */ new Map();
|
|
2673
|
+
return (documentId, effect) => Effect.acquireUseRelease(Effect.sync(() => {
|
|
2674
|
+
let lock = locks.get(documentId);
|
|
2675
|
+
if (!lock) {
|
|
2676
|
+
lock = {
|
|
2677
|
+
semaphore: Semaphore.makeUnsafe(1),
|
|
2678
|
+
users: 0
|
|
2679
|
+
};
|
|
2680
|
+
locks.set(documentId, lock);
|
|
2681
|
+
}
|
|
2682
|
+
lock.users += 1;
|
|
2683
|
+
return lock;
|
|
2684
|
+
}), (lock) => lock.semaphore.withPermit(effect), (lock) => Effect.sync(() => {
|
|
2685
|
+
lock.users -= 1;
|
|
2686
|
+
if (lock.users === 0 && locks.get(documentId) === lock) locks.delete(documentId);
|
|
2687
|
+
}));
|
|
2688
|
+
}
|
|
2689
|
+
|
|
4007
2690
|
//#endregion
|
|
4008
2691
|
//#region ../server/src/internal/working-documents/working-documents.ts
|
|
4009
2692
|
function workingDocumentsLayer(options = {}) {
|
|
@@ -4017,31 +2700,11 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
|
|
|
4017
2700
|
const store = yield* WorkingDocumentStore;
|
|
4018
2701
|
const changeSource = yield* WorkingDocumentChanges;
|
|
4019
2702
|
const changes = yield* Effect.acquireRelease(PubSub.bounded(64), PubSub.shutdown);
|
|
4020
|
-
const
|
|
2703
|
+
const synchronized = documentSynchronization();
|
|
4021
2704
|
const compactAfterUpdates = validCompactionLimit(options.compactAfterUpdates);
|
|
4022
|
-
const synchronized = (documentId, effect) => Effect.suspend(() => {
|
|
4023
|
-
let semaphore = locks.get(documentId);
|
|
4024
|
-
if (!semaphore) {
|
|
4025
|
-
semaphore = Semaphore.makeUnsafe(1);
|
|
4026
|
-
locks.set(documentId, semaphore);
|
|
4027
|
-
}
|
|
4028
|
-
return semaphore.withPermit(effect);
|
|
4029
|
-
});
|
|
4030
2705
|
const generationId = (documentId) => crypto.randomUUIDv4.pipe(Effect.map((uuid) => WorkingDocumentGenerationId.make(uuid)), Effect.mapError((cause) => invalid(documentId, "Could not create a working document generation.", cause)));
|
|
4031
2706
|
const attemptId = (documentId) => crypto.randomUUIDv4.pipe(Effect.map((uuid) => WorkingDocumentPublishAttemptId.make(uuid)), Effect.mapError((cause) => invalid(documentId, "Could not create a publish attempt.", cause)));
|
|
4032
2707
|
const markdownHash = (documentId, markdown) => crypto.digest("SHA-256", new TextEncoder().encode(markdown)).pipe(Effect.map(hex), Effect.mapError((cause) => invalid(documentId, "Could not fingerprint Working Document Markdown.", cause)));
|
|
4033
|
-
const validatedDocument = Effect.fn("WorkingDocuments.validateSource")(function* (source) {
|
|
4034
|
-
const validation = yield* maintenance.validate(UpdateDocumentInput.make({
|
|
4035
|
-
documentId: source.documentId,
|
|
4036
|
-
expectedVersion: source.version,
|
|
4037
|
-
markdown: source.markdown
|
|
4038
|
-
}));
|
|
4039
|
-
if (!validation.valid || !validation.document) return yield* invalid(source.documentId, "Authored document cannot initialize a Working Document.", validation.issues);
|
|
4040
|
-
return validation.document;
|
|
4041
|
-
});
|
|
4042
|
-
const newSnapshot = Effect.fn("WorkingDocuments.newSnapshot")(function* (source, document) {
|
|
4043
|
-
return yield* createWorkingSnapshot(source, document, yield* generationId(source.documentId));
|
|
4044
|
-
});
|
|
4045
2708
|
const compactSnapshot = Effect.fn("WorkingDocuments.compactSnapshot")(function* (snapshot) {
|
|
4046
2709
|
const lastUpdate = snapshot.updates.at(-1);
|
|
4047
2710
|
if (!lastUpdate) return false;
|
|
@@ -4121,21 +2784,21 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
|
|
|
4121
2784
|
const ensureOpenSnapshot = Effect.fn("WorkingDocuments.ensureOpenSnapshot")(function* (source) {
|
|
4122
2785
|
const loaded = yield* store.load(source.documentId);
|
|
4123
2786
|
if (Option.isNone(loaded)) {
|
|
4124
|
-
const document = yield*
|
|
4125
|
-
const initialized = yield* store.initialize(yield*
|
|
2787
|
+
const document = yield* validatedWorkingDocument(source, maintenance.validate);
|
|
2788
|
+
const initialized = yield* store.initialize(yield* newWorkingSnapshot(source, document, generationId));
|
|
4126
2789
|
if (initialized.savedVersion === source.version) return initialized;
|
|
4127
2790
|
if (yield* workingSnapshotIsDirty(initialized)) return initialized;
|
|
4128
2791
|
return yield* store.replace({
|
|
4129
2792
|
expectedGeneration: initialized.generationId,
|
|
4130
|
-
snapshot: yield*
|
|
2793
|
+
snapshot: yield* newWorkingSnapshot(source, document, generationId)
|
|
4131
2794
|
});
|
|
4132
2795
|
}
|
|
4133
2796
|
if (loaded.value.savedVersion === source.version) return loaded.value;
|
|
4134
2797
|
if (yield* workingSnapshotIsDirty(loaded.value)) return loaded.value;
|
|
4135
|
-
const document = yield*
|
|
2798
|
+
const document = yield* validatedWorkingDocument(source, maintenance.validate);
|
|
4136
2799
|
return yield* store.replace({
|
|
4137
2800
|
expectedGeneration: loaded.value.generationId,
|
|
4138
|
-
snapshot: yield*
|
|
2801
|
+
snapshot: yield* newWorkingSnapshot(source, document, generationId)
|
|
4139
2802
|
});
|
|
4140
2803
|
});
|
|
4141
2804
|
const open = (context, documentId) => synchronized(documentId, Effect.gen(function* () {
|
|
@@ -4359,7 +3022,7 @@ function closeHocuspocus(hocuspocus, operations) {
|
|
|
4359
3022
|
const store = hocuspocus.debouncer.executeNow(`onStoreDocument-${document.name}`);
|
|
4360
3023
|
return store === void 0 ? [] : [store];
|
|
4361
3024
|
});
|
|
4362
|
-
yield* Effect.promise(() => Promise.allSettled(stores));
|
|
3025
|
+
const storeResults = yield* Effect.promise(() => Promise.allSettled(stores));
|
|
4363
3026
|
yield* Effect.promise(operations.drain);
|
|
4364
3027
|
const cleanupResults = yield* Effect.promise(() => Promise.allSettled(documents.map((document) => document.saveMutex.runExclusive(() => Promise.resolve()).then(() => hocuspocus.unloadDocument(document)))));
|
|
4365
3028
|
const destroyResult = yield* Effect.result(Effect.tryPromise({
|
|
@@ -4367,6 +3030,8 @@ function closeHocuspocus(hocuspocus, operations) {
|
|
|
4367
3030
|
catch: (cause) => gatewayError("close", cause)
|
|
4368
3031
|
}).pipe(Effect.asVoid));
|
|
4369
3032
|
const cleanupFailure = cleanupResults.find((result) => result.status === "rejected");
|
|
3033
|
+
const storeFailure = storeResults.find((result) => result.status === "rejected");
|
|
3034
|
+
if (storeFailure?.status === "rejected") return yield* gatewayError("close", storeFailure.reason);
|
|
4370
3035
|
if (cleanupFailure?.status === "rejected") return yield* gatewayError("close", cleanupFailure.reason);
|
|
4371
3036
|
if (Result.isFailure(destroyResult)) return yield* destroyResult.failure;
|
|
4372
3037
|
});
|
|
@@ -4380,7 +3045,6 @@ function gatewayError(operation, cause) {
|
|
|
4380
3045
|
|
|
4381
3046
|
//#endregion
|
|
4382
3047
|
//#region ../platform-node/src/internal/realtime/keyed-operations.ts
|
|
4383
|
-
/** Serializes each room independently and retains poisoned writes for shutdown reporting. */
|
|
4384
3048
|
function createKeyedOperations(options) {
|
|
4385
3049
|
const pending = /* @__PURE__ */ new Map();
|
|
4386
3050
|
const failures = /* @__PURE__ */ new Map();
|
|
@@ -4474,7 +3138,6 @@ function stableStringHash(value) {
|
|
|
4474
3138
|
//#region ../platform-node/src/internal/realtime/wake-dispatcher.ts
|
|
4475
3139
|
const DEFAULT_CAPACITY = 1024;
|
|
4476
3140
|
const DEFAULT_CONCURRENCY = 32;
|
|
4477
|
-
/** Bounded wake scheduling: duplicate rooms coalesce and unrelated rooms run concurrently. */
|
|
4478
3141
|
const runStudioRealtimeWakeDispatcher = Effect.fn("StudioRealtimeWakeDispatcher.run")(function* (wakes, handle, options = {}) {
|
|
4479
3142
|
const queue = yield* Queue.dropping(options.capacity ?? DEFAULT_CAPACITY);
|
|
4480
3143
|
const queued = /* @__PURE__ */ new Set();
|
|
@@ -4687,7 +3350,7 @@ const makeStudioRealtimeRoomHost = Effect.fn("StudioRealtimeRoomHost.make")(func
|
|
|
4687
3350
|
},
|
|
4688
3351
|
onStateless: (payload) => runTracked(Effect.gen(function* () {
|
|
4689
3352
|
const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
|
|
4690
|
-
if (decoded
|
|
3353
|
+
if (Option.isNone(decoded) || decoded.value.type !== "checkpoint") return;
|
|
4691
3354
|
const name = yield* decodeRoomName(payload.documentName).pipe(Effect.mapError((cause) => roomHostError("checkpoint", cause)));
|
|
4692
3355
|
const stateVector = Y.encodeStateVector(payload.document);
|
|
4693
3356
|
yield* Effect.tryPromise({
|
|
@@ -4820,7 +3483,9 @@ function decodeBytes(value, tokenInvalid) {
|
|
|
4820
3483
|
try: () => {
|
|
4821
3484
|
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
4822
3485
|
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
4823
|
-
|
|
3486
|
+
const bytes = Uint8Array.from(binary, (character) => character.codePointAt(0) ?? 0);
|
|
3487
|
+
if (encodeBytes(bytes) !== value) throw new Error("Noncanonical base64url encoding");
|
|
3488
|
+
return bytes;
|
|
4824
3489
|
},
|
|
4825
3490
|
catch: tokenInvalid
|
|
4826
3491
|
});
|
|
@@ -4962,7 +3627,7 @@ const makeStudioRealtimeGateway = Effect.gen(function* () {
|
|
|
4962
3627
|
onStateless: (payload) => {
|
|
4963
3628
|
return runTracked(Effect.gen(function* () {
|
|
4964
3629
|
const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
|
|
4965
|
-
if (decoded
|
|
3630
|
+
if (Option.isNone(decoded)) return;
|
|
4966
3631
|
const request = decoded.value;
|
|
4967
3632
|
const context = authenticatedContext(payload.connection.context, payload.documentName);
|
|
4968
3633
|
if (request.type === "checkpoint") return;
|
|
@@ -5172,11 +3837,10 @@ function httpProtocol(protocol) {
|
|
|
5172
3837
|
* StudioRealtimeRoomHost synchronizes durable Yjs state across replicas. Presence
|
|
5173
3838
|
* and stateless broadcasts remain process-local until an ephemeral bus is added.
|
|
5174
3839
|
*/
|
|
5175
|
-
const studioRealtimeGatewayLayer = Layer.effect(StudioRealtimeGateway, Effect.acquireRelease(makeStudioRealtimeGateway, (gateway) => gateway.close.pipe(Effect.
|
|
3840
|
+
const studioRealtimeGatewayLayer = Layer.effect(StudioRealtimeGateway, Effect.acquireRelease(makeStudioRealtimeGateway, (gateway) => gateway.close.pipe(Effect.tapError((error) => Effect.logError("Studio realtime gateway shutdown failed.", error)), Effect.orDie)));
|
|
5176
3841
|
|
|
5177
3842
|
//#endregion
|
|
5178
3843
|
//#region ../platform-node/src/internal/realtime/websocket.ts
|
|
5179
|
-
/** Hides Hocuspocus connection bookkeeping from framework WebSocket adapters. */
|
|
5180
3844
|
function studioRealtimeWebSocketBridge(gateway) {
|
|
5181
3845
|
const connections = /* @__PURE__ */ new WeakMap();
|
|
5182
3846
|
const release = (socket) => {
|
|
@@ -5201,21 +3865,25 @@ function studioRealtimeWebSocketBridge(gateway) {
|
|
|
5201
3865
|
//#endregion
|
|
5202
3866
|
//#region ../platform-node/src/internal/installation/host.ts
|
|
5203
3867
|
var Installation = class extends Context.Service()("@internal/platform-node/Installation") {};
|
|
5204
|
-
function installationHttpLayer(bootstrap,
|
|
3868
|
+
function installationHttpLayer(bootstrap, cleanup) {
|
|
5205
3869
|
return Layer.effect(Installation, Effect.gen(function* () {
|
|
5206
|
-
const outerContext = yield* Effect.context();
|
|
5207
3870
|
const cleanupContext = yield* Effect.context();
|
|
5208
|
-
const
|
|
5209
|
-
const
|
|
5210
|
-
|
|
3871
|
+
const routerConfig = yield* HttpRouter.RouterConfig;
|
|
3872
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
3873
|
+
const handle = HttpEffect.toWebHandlerWith(yield* Effect.context())(router.asHttpEffect(), HttpMiddleware.logger);
|
|
3874
|
+
return yield* makeInstallation(bootstrap, handle, (context) => cleanup(context).pipe(Effect.provide(cleanupContext)), routerConfig);
|
|
5211
3875
|
}));
|
|
5212
3876
|
}
|
|
5213
|
-
const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup) {
|
|
3877
|
+
const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup, routerConfig) {
|
|
5214
3878
|
const authentication = yield* AuthenticationHandler;
|
|
5215
3879
|
const crypto = yield* Crypto.Crypto;
|
|
5216
3880
|
const realtime = yield* StudioRealtimeGateway;
|
|
5217
3881
|
const roomHost = yield* StudioRealtimeRoomHost;
|
|
5218
|
-
const
|
|
3882
|
+
const readiness = yield* Readiness;
|
|
3883
|
+
const protectedRoutes = FindMyWay.make(routerConfig);
|
|
3884
|
+
protectedRoutes.all(CMS_FORM_SUBMIT_PATH, "form");
|
|
3885
|
+
protectedRoutes.all(`${STUDIO_API_PATH}/preview/*`, "preview");
|
|
3886
|
+
const health = readiness.check.pipe(Effect.andThen(roomHost.check), Effect.match({
|
|
5219
3887
|
onFailure: () => ({
|
|
5220
3888
|
live: true,
|
|
5221
3889
|
ready: false
|
|
@@ -5241,13 +3909,15 @@ const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, ha
|
|
|
5241
3909
|
cleanupMedia,
|
|
5242
3910
|
fetch: (request) => {
|
|
5243
3911
|
const pathname = new URL(request.url).pathname;
|
|
3912
|
+
const protection = protectedRoutes.find(request.method, pathname)?.handler;
|
|
3913
|
+
if (protection === "form") return handleFormRequest(request, handle);
|
|
5244
3914
|
if (pathname === "/api/cms/auth" || pathname.startsWith(`${"/api/cms/auth"}/`)) return authentication.handle(request);
|
|
5245
3915
|
if (pathname === CMS_LIVENESS_PATH) return Promise.resolve(healthResponse({
|
|
5246
3916
|
live: true,
|
|
5247
3917
|
ready: true
|
|
5248
3918
|
}, false));
|
|
5249
3919
|
if (pathname === CMS_READINESS_PATH) return runPromise(health).then((status) => healthResponse(status, true));
|
|
5250
|
-
if (
|
|
3920
|
+
if (protection === "preview") return handle(request).then((response) => {
|
|
5251
3921
|
response.headers.set("cache-control", "private, no-store");
|
|
5252
3922
|
return response;
|
|
5253
3923
|
});
|
|
@@ -5295,21 +3965,6 @@ const startInstallation = Effect.fn("CmsInstallation.start")(function* (options)
|
|
|
5295
3965
|
};
|
|
5296
3966
|
});
|
|
5297
3967
|
|
|
5298
|
-
//#endregion
|
|
5299
|
-
//#region ../server/src/internal/media/media-object-store.ts
|
|
5300
|
-
var MediaObjectStore = class extends Context.Service()("@internal/server/MediaObjectStore") {};
|
|
5301
|
-
|
|
5302
|
-
//#endregion
|
|
5303
|
-
//#region ../server/src/internal/media/media-failures.ts
|
|
5304
|
-
var MediaPersistenceError = class extends Schema.TaggedError()("Media.PersistenceError", {
|
|
5305
|
-
operation: Schema.NonEmptyString,
|
|
5306
|
-
cause: Schema.Defect()
|
|
5307
|
-
}) {};
|
|
5308
|
-
var MediaStorageError = class extends Schema.TaggedError()("Media.StorageError", {
|
|
5309
|
-
operation: Schema.NonEmptyString,
|
|
5310
|
-
cause: Schema.Defect()
|
|
5311
|
-
}) {};
|
|
5312
|
-
|
|
5313
3968
|
//#endregion
|
|
5314
3969
|
//#region ../server/src/internal/media/media-change.ts
|
|
5315
3970
|
const MediaUploadIntent = Schema.Struct({
|
|
@@ -5472,7 +4127,7 @@ function makeMediaChange(input) {
|
|
|
5472
4127
|
action: input.action,
|
|
5473
4128
|
context: input.change.context,
|
|
5474
4129
|
id: input.change.id,
|
|
5475
|
-
intent: Schema.
|
|
4130
|
+
intent: Result.getOrThrow(Schema.encodeResult(MediaChangeIntent)(input.intent)),
|
|
5476
4131
|
scope: "media",
|
|
5477
4132
|
subject: input.subject,
|
|
5478
4133
|
target: input.target
|
|
@@ -5481,55 +4136,43 @@ function makeMediaChange(input) {
|
|
|
5481
4136
|
|
|
5482
4137
|
//#endregion
|
|
5483
4138
|
//#region ../server/src/internal/media/media-reconciliation.ts
|
|
5484
|
-
|
|
4139
|
+
const recoveryBatchSize = 100;
|
|
4140
|
+
function recoverPendingMedia(store, objectStore, operations, recoveryAge, cursor = {}) {
|
|
5485
4141
|
return Effect.gen(function* () {
|
|
5486
4142
|
const before = DateTime.subtractDuration(yield* DateTime.now, recoveryAge);
|
|
5487
|
-
const
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
4143
|
+
const pending = yield* store.pending({
|
|
4144
|
+
after: cursor.media,
|
|
4145
|
+
before,
|
|
4146
|
+
limit: recoveryBatchSize
|
|
4147
|
+
});
|
|
4148
|
+
for (const record of pending) {
|
|
4149
|
+
if (record.status === "deleting") {
|
|
4150
|
+
yield* objectStore.remove(record.objectKey);
|
|
4151
|
+
yield* store.finishDelete(record.id);
|
|
4152
|
+
continue;
|
|
4153
|
+
}
|
|
4154
|
+
const stored = yield* objectStore.stat(record.objectKey);
|
|
4155
|
+
if (Option.isSome(stored) && stored.value.byteSize === record.byteSize && (stored.value.mimeType === void 0 || stored.value.mimeType === record.mimeType)) yield* store.activate(record.id);
|
|
4156
|
+
else {
|
|
4157
|
+
if (Option.isSome(stored)) {
|
|
5495
4158
|
yield* objectStore.remove(record.objectKey);
|
|
5496
|
-
yield*
|
|
5497
|
-
continue;
|
|
5498
|
-
}
|
|
5499
|
-
const stored = yield* objectStore.stat(record.objectKey);
|
|
5500
|
-
if (Option.isSome(stored) && stored.value.byteSize === record.byteSize && (stored.value.mimeType === void 0 || stored.value.mimeType === record.mimeType)) yield* store.activate(record.id);
|
|
5501
|
-
else {
|
|
5502
|
-
if (Option.isSome(stored)) {
|
|
5503
|
-
yield* objectStore.remove(record.objectKey);
|
|
5504
|
-
const remaining = yield* objectStore.stat(record.objectKey);
|
|
5505
|
-
if (Option.isSome(remaining)) continue;
|
|
5506
|
-
}
|
|
5507
|
-
yield* store.abandonUpload(record.id);
|
|
4159
|
+
const remaining = yield* objectStore.stat(record.objectKey);
|
|
4160
|
+
if (Option.isSome(remaining)) continue;
|
|
5508
4161
|
}
|
|
4162
|
+
yield* store.abandonUpload(record.id);
|
|
5509
4163
|
}
|
|
5510
|
-
return pending.length < 100;
|
|
5511
|
-
});
|
|
5512
|
-
let mediaBatch = 0;
|
|
5513
|
-
let mediaComplete = false;
|
|
5514
|
-
while (!mediaComplete && mediaBatch < maxBatches) {
|
|
5515
|
-
mediaComplete = yield* recoverBatch;
|
|
5516
|
-
mediaBatch += 1;
|
|
5517
4164
|
}
|
|
5518
|
-
const
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
});
|
|
5523
|
-
yield* Effect.forEach(replacements, (replacement) => recoverReplacement(store, objectStore, replacement).pipe(Effect.catchTag("Media.Conflict", () => Effect.void)), { discard: true });
|
|
5524
|
-
return replacements.length < 100;
|
|
4165
|
+
const replacements = yield* store.pendingReplacements({
|
|
4166
|
+
after: cursor.replacements,
|
|
4167
|
+
before,
|
|
4168
|
+
limit: recoveryBatchSize
|
|
5525
4169
|
});
|
|
5526
|
-
|
|
5527
|
-
let replacementsComplete = false;
|
|
5528
|
-
while (!replacementsComplete && replacementBatch < maxBatches) {
|
|
5529
|
-
replacementsComplete = yield* recoverReplacementBatch;
|
|
5530
|
-
replacementBatch += 1;
|
|
5531
|
-
}
|
|
4170
|
+
yield* Effect.forEach(replacements, (replacement) => recoverReplacement(store, objectStore, replacement).pipe(Effect.catchTag("Media.Conflict", () => Effect.void)), { discard: true });
|
|
5532
4171
|
yield* recoverPendingMediaCleanup(operations, objectStore, before);
|
|
4172
|
+
return {
|
|
4173
|
+
media: pending.length < recoveryBatchSize ? void 0 : pending.at(-1)?.id,
|
|
4174
|
+
replacements: replacements.length < recoveryBatchSize ? void 0 : replacements.at(-1)?.mediaId
|
|
4175
|
+
};
|
|
5533
4176
|
});
|
|
5534
4177
|
}
|
|
5535
4178
|
function recoverPendingMediaCleanup(operations, objectStore, before) {
|
|
@@ -5717,10 +4360,14 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
|
|
|
5717
4360
|
const store = yield* MediaStore;
|
|
5718
4361
|
const baseUrl = options.publicBaseUrl.replace(/\/+$/gu, "");
|
|
5719
4362
|
const pendingRecoveryAge = options.pendingRecoveryAge ?? "5 minutes";
|
|
5720
|
-
yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge);
|
|
4363
|
+
const recoveryCursor = yield* Ref.make(yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge));
|
|
5721
4364
|
const mutations = yield* Semaphore.make(1);
|
|
5722
4365
|
const recoveryInterval = options.pendingRecoveryInterval ?? MEDIA_OPERATION_RECOVERY_INTERVAL;
|
|
5723
|
-
if (recoveryInterval !== false) yield* mutations.withPermits(1)(
|
|
4366
|
+
if (recoveryInterval !== false) yield* mutations.withPermits(1)(Effect.gen(function* () {
|
|
4367
|
+
const cursor = yield* Ref.get(recoveryCursor);
|
|
4368
|
+
const next = yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge, cursor);
|
|
4369
|
+
yield* Ref.set(recoveryCursor, next);
|
|
4370
|
+
})).pipe(Effect.catch((error) => Effect.logWarning("Pending media change recovery sweep failed.", { errorTag: error._tag }))).pipe(Effect.repeat(Schedule.spaced(recoveryInterval)), Effect.delay(recoveryInterval), Effect.forkScoped({ startImmediately: true }));
|
|
5724
4371
|
const toAsset = (record) => MediaAsset.make({
|
|
5725
4372
|
byteSize: record.byteSize,
|
|
5726
4373
|
createdAt: record.createdAt,
|
|
@@ -5789,7 +4436,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
|
|
|
5789
4436
|
return yield* Effect.scoped(prepareUpload(input).pipe(Effect.flatMap((prepared) => mutations.withPermits(1)(Effect.gen(function* () {
|
|
5790
4437
|
const id = MediaId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
5791
4438
|
const operationId = ActivityId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
5792
|
-
const objectKey = MediaObjectKey.make(
|
|
4439
|
+
const objectKey = MediaObjectKey.make(`${mediaObjectPrefix}${id}.${imageExtension(prepared.mimeType)}`);
|
|
5793
4440
|
const now = yield* DateTime.now;
|
|
5794
4441
|
const reserved = yield* store.reserve({
|
|
5795
4442
|
byteSize: input.metadata.declaredByteSize,
|
|
@@ -5837,7 +4484,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
|
|
|
5837
4484
|
name: input.metadata.name,
|
|
5838
4485
|
objectKey: current.value.objectKey,
|
|
5839
4486
|
staged: false,
|
|
5840
|
-
stagingObjectKey: MediaObjectKey.make(
|
|
4487
|
+
stagingObjectKey: MediaObjectKey.make(`${mediaObjectPrefix}replacements/${operationId}.${imageExtension(prepared.mimeType)}`),
|
|
5841
4488
|
updatedAt: timestamp
|
|
5842
4489
|
};
|
|
5843
4490
|
const pendingReplacement = yield* store.beginReplace(replacement, {
|
|
@@ -5960,11 +4607,42 @@ function cleanupBeforePromotionBestEffort(cleanup) {
|
|
|
5960
4607
|
onSuccess: () => Effect.void
|
|
5961
4608
|
}));
|
|
5962
4609
|
}
|
|
5963
|
-
function activityFailure(operation) {
|
|
5964
|
-
return (cause) => new ActivityUnavailable({
|
|
5965
|
-
cause,
|
|
5966
|
-
operation
|
|
4610
|
+
function activityFailure(operation) {
|
|
4611
|
+
return (cause) => new ActivityUnavailable({
|
|
4612
|
+
cause,
|
|
4613
|
+
operation
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
//#endregion
|
|
4618
|
+
//#region ../server/src/ProductModules.ts
|
|
4619
|
+
function productModulesLayer(options) {
|
|
4620
|
+
const accessControl = defaultAccessControlLayer;
|
|
4621
|
+
const seo = seoLayer(options.bootstrap.seo ?? makeDefaultSeoOptions(options.baseUrl)).pipe(Layer.provide(accessControl));
|
|
4622
|
+
const authoredDocuments = documentsLayer(options.bootstrap.documents, {
|
|
4623
|
+
...options.documentEngine,
|
|
4624
|
+
components: options.documentEngine.components ?? [],
|
|
4625
|
+
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? "https://media.invalid"
|
|
4626
|
+
}).pipe(Layer.provide(accessControl));
|
|
4627
|
+
const documentAccess = Layer.merge(authoredDocuments, accessControl);
|
|
4628
|
+
const activity = activityLayer.pipe(Layer.provide(accessControl));
|
|
4629
|
+
const workingDocuments = workingDocumentsLayer(options.workingDocuments).pipe(Layer.provide(documentAccess));
|
|
4630
|
+
const seoDelivery = seoDeliveryLayer.pipe(Layer.provide(Layer.merge(documentAccess, seo)));
|
|
4631
|
+
return Layer.mergeAll(activity, documentAccess, workingDocuments, seo, seoDelivery, readinessLayer);
|
|
4632
|
+
}
|
|
4633
|
+
function productModulesWithMediaLayer(options) {
|
|
4634
|
+
const products = productModulesLayer({
|
|
4635
|
+
...options,
|
|
4636
|
+
documentEngine: {
|
|
4637
|
+
...options.documentEngine,
|
|
4638
|
+
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? options.mediaPublicBaseUrl
|
|
4639
|
+
}
|
|
5967
4640
|
});
|
|
4641
|
+
const media = mediaLibraryLayer({
|
|
4642
|
+
maxUploadBytes: options.bootstrap.media.maxUploadBytes,
|
|
4643
|
+
publicBaseUrl: options.mediaPublicBaseUrl
|
|
4644
|
+
}).pipe(Layer.provide(products));
|
|
4645
|
+
return Layer.merge(products, media);
|
|
5968
4646
|
}
|
|
5969
4647
|
|
|
5970
4648
|
//#endregion
|
|
@@ -5974,7 +4652,6 @@ var Initialization = class extends Context.Service()("@internal/server/Initializ
|
|
|
5974
4652
|
function initializationLayer(options) {
|
|
5975
4653
|
return Layer.effect(Initialization, Effect.gen(function* () {
|
|
5976
4654
|
const journal = yield* ChangeJournal;
|
|
5977
|
-
const documentOperations = yield* DocumentMaintenance;
|
|
5978
4655
|
const crypto = yield* Crypto.Crypto;
|
|
5979
4656
|
const identity = yield* InitialIdentity;
|
|
5980
4657
|
const recovery = yield* InitialIdentityRecovery;
|
|
@@ -6013,15 +4690,6 @@ function initializationLayer(options) {
|
|
|
6013
4690
|
intent: prepared.intent
|
|
6014
4691
|
});
|
|
6015
4692
|
if (!(yield* store.claim(durableClaim))) return yield* new InitializationAlreadyCompleted();
|
|
6016
|
-
const imported = yield* documentOperations.importSources(DocumentImportInput.make({
|
|
6017
|
-
mode: "preserve",
|
|
6018
|
-
reason: "CMS initialization",
|
|
6019
|
-
sources: options.initialSources ?? []
|
|
6020
|
-
}), context).pipe(Effect.result);
|
|
6021
|
-
if (Result.isFailure(imported)) {
|
|
6022
|
-
yield* store.reset(changeId);
|
|
6023
|
-
return yield* imported.failure;
|
|
6024
|
-
}
|
|
6025
4693
|
const begin = yield* journal.begin(initialAdministratorDraft({
|
|
6026
4694
|
context,
|
|
6027
4695
|
id: changeId
|
|
@@ -6067,20 +4735,21 @@ const reconcileStored = Effect.fn("Initialization.reconcileStored")(function* (s
|
|
|
6067
4735
|
id: state.claim.changeId
|
|
6068
4736
|
}, state.claim.intent);
|
|
6069
4737
|
const begin = yield* journal.begin(draft).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
case "Unresolved": {
|
|
6078
|
-
const resolution = yield* reconcileInitialAdministrator(journal, begin.change, recovery.observe).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
|
|
6079
|
-
if (resolution.kind === "applied") yield* store.complete(state.claim.changeId);
|
|
6080
|
-
if (resolution.kind === "cancelled") yield* store.reset(state.claim.changeId);
|
|
6081
|
-
return yield* store.read;
|
|
6082
|
-
}
|
|
4738
|
+
if (Predicate.isTagged("Recorded")(begin)) {
|
|
4739
|
+
yield* store.complete(state.claim.changeId);
|
|
4740
|
+
return yield* store.read;
|
|
4741
|
+
}
|
|
4742
|
+
if (Predicate.isTagged("Cancelled")(begin)) {
|
|
4743
|
+
yield* store.reset(state.claim.changeId);
|
|
4744
|
+
return yield* store.read;
|
|
6083
4745
|
}
|
|
4746
|
+
if (Predicate.isTagged("Unresolved")(begin)) {
|
|
4747
|
+
const resolution = yield* reconcileInitialAdministrator(journal, begin.change, recovery.observe).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
|
|
4748
|
+
if (resolution.kind === "applied") yield* store.complete(state.claim.changeId);
|
|
4749
|
+
if (resolution.kind === "cancelled") yield* store.reset(state.claim.changeId);
|
|
4750
|
+
return yield* store.read;
|
|
4751
|
+
}
|
|
4752
|
+
return yield* store.read;
|
|
6084
4753
|
});
|
|
6085
4754
|
function completeBestEffort(store, changeId) {
|
|
6086
4755
|
return store.complete(changeId).pipe(Effect.catch((error) => Effect.logWarning("Initialization completion remains recoverable.", {
|
|
@@ -6281,6 +4950,69 @@ function targetError(operation) {
|
|
|
6281
4950
|
return (error) => Schema.is(UserAlreadyExists)(error) ? unavailable$2(operation)(error) : error;
|
|
6282
4951
|
}
|
|
6283
4952
|
|
|
4953
|
+
//#endregion
|
|
4954
|
+
//#region ../server/src/Forms.ts
|
|
4955
|
+
/** Only the currently published definition can authorize a submission. Never trust client rules. */
|
|
4956
|
+
const submitForm = Effect.fn("Forms.submit")(function* (input, components, handlers) {
|
|
4957
|
+
const document = yield* (yield* PublishedDocuments).read({
|
|
4958
|
+
kind: "id",
|
|
4959
|
+
documentId: input.documentId
|
|
4960
|
+
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
4961
|
+
if (!document) return FormResult.make({
|
|
4962
|
+
status: "unavailable",
|
|
4963
|
+
errors: {}
|
|
4964
|
+
});
|
|
4965
|
+
const form = documentForms(document, components).find((candidate) => JSON.stringify(candidate.nodePath) === JSON.stringify(input.nodePath));
|
|
4966
|
+
if (!form || form.revision !== input.revision) return FormResult.make({
|
|
4967
|
+
status: "stale",
|
|
4968
|
+
errors: {}
|
|
4969
|
+
});
|
|
4970
|
+
const handler = Object.hasOwn(handlers, form.name) ? handlers[form.name] : void 0;
|
|
4971
|
+
if (!handler) return FormResult.make({
|
|
4972
|
+
status: "unavailable",
|
|
4973
|
+
errors: {}
|
|
4974
|
+
});
|
|
4975
|
+
const fields = form.steps.flatMap((step) => step.fields);
|
|
4976
|
+
const errors = validateFormValues(fields, input.values);
|
|
4977
|
+
if (Object.keys(input.values).some((id) => !fields.some((field) => field.id === id))) errors._form = "The submission contains unknown answers.";
|
|
4978
|
+
if (Object.keys(errors).length) return FormResult.make({
|
|
4979
|
+
status: "invalid",
|
|
4980
|
+
errors
|
|
4981
|
+
});
|
|
4982
|
+
const values = Object.fromEntries(fields.map((field) => [field.id, normalizedAnswer(Object.hasOwn(input.values, field.id) ? input.values[field.id] : void 0)]));
|
|
4983
|
+
return yield* Effect.tryPromise(() => Promise.resolve(handler({
|
|
4984
|
+
form,
|
|
4985
|
+
values
|
|
4986
|
+
}))).pipe(Effect.as(FormResult.make({
|
|
4987
|
+
status: "accepted",
|
|
4988
|
+
errors: {}
|
|
4989
|
+
})), Effect.orElseSucceed(() => FormResult.make({
|
|
4990
|
+
status: "failed",
|
|
4991
|
+
errors: {}
|
|
4992
|
+
})));
|
|
4993
|
+
});
|
|
4994
|
+
function normalizedAnswer(value) {
|
|
4995
|
+
return value === void 0 || Predicate.isString(value) && !value.trim() ? null : value;
|
|
4996
|
+
}
|
|
4997
|
+
|
|
4998
|
+
//#endregion
|
|
4999
|
+
//#region ../platform-node/src/internal/http/forms.ts
|
|
5000
|
+
function formsApiLayer(components, handlers) {
|
|
5001
|
+
return HttpApiBuilder.group(StudioApi, "forms", (group) => Effect.gen(function* () {
|
|
5002
|
+
const documents = yield* PublishedDocuments;
|
|
5003
|
+
const limiter = yield* RateLimiter.RateLimiter;
|
|
5004
|
+
return group.handle("submit", ({ payload }) => limiter.consume({
|
|
5005
|
+
key: "cms-forms",
|
|
5006
|
+
limit: 60,
|
|
5007
|
+
window: "1 minute",
|
|
5008
|
+
onExceeded: "fail"
|
|
5009
|
+
}).pipe(Effect.andThen(submitForm(payload, components, handlers).pipe(Effect.provideService(PublishedDocuments, documents))), Effect.orElseSucceed(() => FormResult.make({
|
|
5010
|
+
status: "unavailable",
|
|
5011
|
+
errors: {}
|
|
5012
|
+
}))));
|
|
5013
|
+
})).pipe(Layer.provide(RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory))));
|
|
5014
|
+
}
|
|
5015
|
+
|
|
6284
5016
|
//#endregion
|
|
6285
5017
|
//#region ../platform-node/src/internal/http/errors.ts
|
|
6286
5018
|
const studioSchemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(StudioSchemaError, (error) => Effect.fail(mapStudioSchemaError(error.kind)));
|
|
@@ -6345,6 +5077,7 @@ const isAccessDenied$1 = Schema.is(AccessDenied);
|
|
|
6345
5077
|
const isDocumentNotFound$1 = Schema.is(DocumentNotFound);
|
|
6346
5078
|
const isDocumentVersionConflict = Schema.is(DocumentVersionConflict);
|
|
6347
5079
|
const isDocumentGenerationConflict = Schema.is(DocumentGenerationConflict);
|
|
5080
|
+
const isContentTransferConflict = Schema.is(ContentTransferConflict);
|
|
6348
5081
|
const isPublicMutationError = Schema.is(Schema.Union([
|
|
6349
5082
|
AccessDenied,
|
|
6350
5083
|
DocumentMediaUnavailable,
|
|
@@ -6384,6 +5117,10 @@ const documentsApiLayer = HttpApiBuilder.group(StudioApi, "documents", (handlers
|
|
|
6384
5117
|
...payload
|
|
6385
5118
|
}))).pipe(mapMutationError));
|
|
6386
5119
|
}));
|
|
5120
|
+
const contentApiLayer = HttpApiBuilder.group(StudioApi, "content", (handlers) => Effect.gen(function* () {
|
|
5121
|
+
const documents = yield* Documents;
|
|
5122
|
+
return handlers.handle("snapshot", () => withStudioRequest((context) => documents.contentSnapshot(context)).pipe(preservePublicErrors(isAccessDenied$1))).handle("plan", ({ payload }) => withStudioRequest((context) => documents.contentPlan(context, payload)).pipe(preservePublicErrors((error) => isAccessDenied$1(error) || isContentTransferConflict(error)))).handle("apply", ({ payload }) => withStudioRequest((context) => documents.contentApply(context, payload)).pipe(preservePublicErrors((error) => isAccessDenied$1(error) || isContentTransferConflict(error) || Schema.is(DocumentValidationFailed)(error))));
|
|
5123
|
+
}));
|
|
6387
5124
|
function mapMutationError(effect) {
|
|
6388
5125
|
return effect.pipe(Effect.mapError((error) => {
|
|
6389
5126
|
if (isPublicMutationError(error)) return error;
|
|
@@ -6462,7 +5199,7 @@ function decodeMediaUpload(payload, query, maxUploadBytes) {
|
|
|
6462
5199
|
}
|
|
6463
5200
|
function mapMultipartError(error) {
|
|
6464
5201
|
if (isMediaInvalid(error)) return error;
|
|
6465
|
-
return
|
|
5202
|
+
return Predicate.isTagged("InternalError")(error.reason) ? studioInternalError() : invalidMultipart();
|
|
6466
5203
|
}
|
|
6467
5204
|
function invalidMultipart() {
|
|
6468
5205
|
return new MediaInvalid({ message: "Upload exactly one file whose body matches its declared size." });
|
|
@@ -6474,7 +5211,16 @@ const publicError = Schema.is(Schema.Union([AccessDenied, PublishedDocumentNotFo
|
|
|
6474
5211
|
const denied = Schema.is(AccessDenied);
|
|
6475
5212
|
const previewApiLayer = HttpApiBuilder.group(StudioApi, "previewDelivery", (handlers) => Effect.gen(function* () {
|
|
6476
5213
|
const documents = yield* DocumentPreviews;
|
|
6477
|
-
|
|
5214
|
+
const accessControl = yield* AccessControl;
|
|
5215
|
+
const seo = yield* PreviewSeo;
|
|
5216
|
+
const seoDelivery = yield* SeoDelivery;
|
|
5217
|
+
return handlers.handle("read", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.read(context, payload).pipe(Effect.flatMap((document) => seoDelivery.previewDocument(document)))), preservePublicErrors(publicError))).handle("query", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.query(context, payload).pipe(Effect.flatMap((page) => Effect.forEach(page.items, (document) => seoDelivery.previewDocument(document)).pipe(Effect.map((items) => Object.assign({}, page, { items })))))), preservePublicErrors(denied))).handle("navigation", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.navigation(context, payload)), preservePublicErrors(denied))).handle("surround", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.surround(context, payload.lookup, payload.query).pipe(Effect.flatMap((surround) => Effect.all({
|
|
5218
|
+
previous: surround.previous ? seoDelivery.previewDocument(surround.previous) : Effect.succeed(null),
|
|
5219
|
+
next: surround.next ? seoDelivery.previewDocument(surround.next) : Effect.succeed(null)
|
|
5220
|
+
})))), preservePublicErrors(publicError))).handle("seo", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => accessControl.require(context, {
|
|
5221
|
+
action: "document.browse",
|
|
5222
|
+
resource: { kind: "catalog" }
|
|
5223
|
+
}).pipe(Effect.andThen(seo.read))), preservePublicErrors(denied)));
|
|
6478
5224
|
}));
|
|
6479
5225
|
|
|
6480
5226
|
//#endregion
|
|
@@ -6482,7 +5228,15 @@ const previewApiLayer = HttpApiBuilder.group(StudioApi, "previewDelivery", (hand
|
|
|
6482
5228
|
const isPublishedDocumentNotFound = Schema.is(PublishedDocumentNotFound);
|
|
6483
5229
|
const deliveryApiLayer = Layer.merge(HttpApiBuilder.group(StudioApi, "delivery", (handlers) => Effect.gen(function* () {
|
|
6484
5230
|
const documents = yield* PublishedDocuments;
|
|
6485
|
-
|
|
5231
|
+
const seo = yield* PublishedSeo;
|
|
5232
|
+
const seoDelivery = yield* SeoDelivery;
|
|
5233
|
+
return handlers.handle("read", ({ payload }) => documents.read(payload).pipe(Effect.flatMap(seoDelivery.publishedDocument), preservePublicErrors(isPublishedDocumentNotFound))).handle("query", ({ payload }) => documents.query(payload).pipe(Effect.flatMap((page) => Effect.forEach(page.items, seoDelivery.publishedDocument).pipe(Effect.map((items) => ({
|
|
5234
|
+
items,
|
|
5235
|
+
total: page.total
|
|
5236
|
+
})))), mapUnexpectedError)).handle("navigation", ({ payload }) => documents.navigation(payload).pipe(mapUnexpectedError)).handle("surround", ({ payload }) => documents.surround(payload.lookup, payload.query).pipe(Effect.flatMap((surround) => Effect.all({
|
|
5237
|
+
previous: surround.previous ? seoDelivery.publishedDocument(surround.previous) : Effect.succeed(null),
|
|
5238
|
+
next: surround.next ? seoDelivery.publishedDocument(surround.next) : Effect.succeed(null)
|
|
5239
|
+
})), preservePublicErrors(isPublishedDocumentNotFound))).handle("seo", () => seo.read.pipe(mapUnexpectedError)).handle("sitemap", () => seoDelivery.sitemap.pipe(mapUnexpectedError));
|
|
6486
5240
|
})), HttpApiBuilder.group(StudioApi, "diagnostics", (handlers) => Effect.gen(function* () {
|
|
6487
5241
|
const documents = yield* PublishedDocuments;
|
|
6488
5242
|
return handlers.handle("documents", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.diagnostics(context)), preservePublicErrors(Schema.is(AccessDenied))));
|
|
@@ -6498,6 +5252,12 @@ const isHistoryPublicError = Schema.is(Schema.Union([
|
|
|
6498
5252
|
DocumentNotFound
|
|
6499
5253
|
]));
|
|
6500
5254
|
const isInitializationPublicError = Schema.is(Schema.Union([InitializationAlreadyCompleted, InitializationProofInvalid]));
|
|
5255
|
+
const isSeoPublicError = Schema.is(Schema.Union([
|
|
5256
|
+
AccessDenied,
|
|
5257
|
+
SeoConfirmationRequired,
|
|
5258
|
+
SeoMediaUnavailable,
|
|
5259
|
+
SeoVersionConflict
|
|
5260
|
+
]));
|
|
6501
5261
|
const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "initialization", (handlers) => Effect.gen(function* () {
|
|
6502
5262
|
const initialization = yield* Initialization;
|
|
6503
5263
|
return handlers.handle("status", () => initialization.status.pipe(mapUnexpectedError)).handle("claim", ({ payload }) => initialization.claim(payload).pipe(preservePublicErrors(isInitializationPublicError)));
|
|
@@ -6507,6 +5267,9 @@ const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "i
|
|
|
6507
5267
|
})), HttpApiBuilder.group(StudioApi, "activity", (handlers) => Effect.gen(function* () {
|
|
6508
5268
|
const activity = yield* Activity;
|
|
6509
5269
|
return handlers.handle("list", ({ query }) => withStudioRequest((context) => activity.list(context, query)).pipe(preservePublicErrors(isActivityPublicError)));
|
|
5270
|
+
})), HttpApiBuilder.group(StudioApi, "seo", (handlers) => Effect.gen(function* () {
|
|
5271
|
+
const seo = yield* Seo;
|
|
5272
|
+
return handlers.handle("read", () => withStudioRequest((context) => seo.read(context)).pipe(preservePublicErrors(isSeoPublicError))).handle("save", ({ payload }) => withStudioRequest((context) => seo.save(context, payload)).pipe(preservePublicErrors(isSeoPublicError))).handle("publish", ({ payload }) => withStudioRequest((context) => seo.publish(context, payload)).pipe(preservePublicErrors(isSeoPublicError)));
|
|
6510
5273
|
})));
|
|
6511
5274
|
|
|
6512
5275
|
//#endregion
|
|
@@ -6532,43 +5295,14 @@ const userAdministrationHttpLayer = HttpApiBuilder.group(StudioApi, "users", (ha
|
|
|
6532
5295
|
|
|
6533
5296
|
//#endregion
|
|
6534
5297
|
//#region ../platform-node/src/internal/http/api.ts
|
|
6535
|
-
function studioApiLayer(bootstrap, authoringRevision) {
|
|
5298
|
+
function studioApiLayer(bootstrap, authoringRevision, components = [], formHandlers = {}) {
|
|
6536
5299
|
const configuration = HttpApiBuilder.group(StudioApi, "configuration", (handlers) => handlers.handle("bootstrap", () => Effect.succeed(bootstrap)));
|
|
6537
|
-
return authenticatedRouterHandlers(Layer.mergeAll(configuration, documentsApiLayer, mediaApiLayer(bootstrap.media.enabled, bootstrap.media.maxUploadBytes), collaborationApiLayer(authoringRevision), deliveryApiLayer, previewApiLayer, administrationApiLayer, userAdministrationHttpLayer).pipe(Layer.provideMerge(studioSchemaErrorLayer)));
|
|
5300
|
+
return authenticatedRouterHandlers(Layer.mergeAll(configuration, formsApiLayer(components, formHandlers), documentsApiLayer, contentApiLayer, mediaApiLayer(bootstrap.media.enabled, bootstrap.media.maxUploadBytes), collaborationApiLayer(authoringRevision), deliveryApiLayer, previewApiLayer, administrationApiLayer, userAdministrationHttpLayer).pipe(Layer.provideMerge(studioSchemaErrorLayer)));
|
|
6538
5301
|
}
|
|
6539
5302
|
function authenticatedRouterHandlers(handlers) {
|
|
6540
5303
|
return handlers;
|
|
6541
5304
|
}
|
|
6542
5305
|
|
|
6543
|
-
//#endregion
|
|
6544
|
-
//#region ../platform-node/src/internal/installation/server-modules.ts
|
|
6545
|
-
function productModulesLayer(options) {
|
|
6546
|
-
const accessControl = defaultAccessControlLayer;
|
|
6547
|
-
const authoredDocuments = documentsLayer(options.bootstrap.documents, {
|
|
6548
|
-
...options.documentEngine,
|
|
6549
|
-
components: options.documentEngine.components ?? [],
|
|
6550
|
-
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? "https://media.invalid"
|
|
6551
|
-
}).pipe(Layer.provide(accessControl));
|
|
6552
|
-
const documentAccess = Layer.merge(authoredDocuments, accessControl);
|
|
6553
|
-
const activity = activityLayer.pipe(Layer.provide(accessControl));
|
|
6554
|
-
const workingDocuments = workingDocumentsLayer(options.workingDocuments).pipe(Layer.provide(documentAccess));
|
|
6555
|
-
return Layer.mergeAll(activity, documentAccess, workingDocuments);
|
|
6556
|
-
}
|
|
6557
|
-
function productModulesWithMediaLayer(options) {
|
|
6558
|
-
const products = productModulesLayer({
|
|
6559
|
-
...options,
|
|
6560
|
-
documentEngine: {
|
|
6561
|
-
...options.documentEngine,
|
|
6562
|
-
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? options.mediaPublicBaseUrl
|
|
6563
|
-
}
|
|
6564
|
-
});
|
|
6565
|
-
const media = mediaLibraryLayer({
|
|
6566
|
-
maxUploadBytes: options.bootstrap.media.maxUploadBytes,
|
|
6567
|
-
publicBaseUrl: options.mediaPublicBaseUrl
|
|
6568
|
-
}).pipe(Layer.provide(products));
|
|
6569
|
-
return Layer.merge(products, media);
|
|
6570
|
-
}
|
|
6571
|
-
|
|
6572
5306
|
//#endregion
|
|
6573
5307
|
//#region ../platform-node/src/internal/installation/error.ts
|
|
6574
5308
|
var CmsInstallationError = class extends Schema.TaggedError()("Cms.InstallationError", {
|
|
@@ -6585,6 +5319,7 @@ function makeInstallationLayer(validated) {
|
|
|
6585
5319
|
const options = validated.options;
|
|
6586
5320
|
const core = options.composition.core;
|
|
6587
5321
|
const productOptions = {
|
|
5322
|
+
baseUrl: options.baseUrl,
|
|
6588
5323
|
bootstrap: options.bootstrap,
|
|
6589
5324
|
documentEngine: {
|
|
6590
5325
|
documentModel: options.documentModel,
|
|
@@ -6598,18 +5333,15 @@ function makeInstallationLayer(validated) {
|
|
|
6598
5333
|
}).pipe(Layer.provide(Layer.merge(core, media.objects)));
|
|
6599
5334
|
const productModules = validated.media.enabled ? enabledProductModules(validated.media.configuration) : productModulesLayer(productOptions).pipe(Layer.provide(core));
|
|
6600
5335
|
const authentication = studioAuthenticationLayer.pipe(Layer.provide(core));
|
|
6601
|
-
const initialization = initializationLayer({
|
|
6602
|
-
initialSources: options.initialSources,
|
|
6603
|
-
initializationToken: options.initializationToken
|
|
6604
|
-
}).pipe(Layer.provide(Layer.merge(productModules, core)));
|
|
5336
|
+
const initialization = initializationLayer({ initializationToken: options.initializationToken }).pipe(Layer.provide(core));
|
|
6605
5337
|
const users = userAdministrationLayer.pipe(Layer.provide(core));
|
|
6606
5338
|
const tickets = studioRealtimeTicketsLayer(options.studioSecret).pipe(Layer.provide(core));
|
|
6607
5339
|
const roomHost = studioRealtimeRoomHostLayer().pipe(Layer.provide(productModules));
|
|
6608
5340
|
const gateway = studioRealtimeGatewayLayer.pipe(Layer.provide(Layer.mergeAll(productModules, roomHost, tickets)));
|
|
6609
5341
|
const httpDependencies = Layer.mergeAll(core, productModules, gateway, initialization, roomHost, users).pipe(Layer.provideMerge(authentication), Layer.provideMerge(tickets));
|
|
6610
|
-
const handlers = studioApiLayer(options.bootstrap, options.authoring.revision).pipe(Layer.provide(httpDependencies));
|
|
6611
|
-
const httpApi = HttpApiBuilder.layer(StudioApi).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices));
|
|
6612
|
-
return validated.media.enabled ? installationHttpLayer(options.bootstrap,
|
|
5342
|
+
const handlers = studioApiLayer(options.bootstrap, options.authoring.revision, options.authoring.components, options.formHandlers).pipe(Layer.provide(httpDependencies));
|
|
5343
|
+
const httpApi = HttpApiBuilder.layer(StudioApi).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer));
|
|
5344
|
+
return validated.media.enabled ? installationHttpLayer(options.bootstrap, (context) => Effect.flatMap(MediaLibrary, (library) => library.cleanup(context))).pipe(Layer.provide(Layer.merge(httpDependencies, httpApi))) : installationHttpLayer(options.bootstrap, () => Effect.succeed(MediaCleanupReport.make({ removedObjectKeys: [] }))).pipe(Layer.provide(Layer.merge(httpDependencies, httpApi)));
|
|
6613
5345
|
}
|
|
6614
5346
|
function validateInstallationOptions(options) {
|
|
6615
5347
|
if (options.bootstrap.media.enabled) {
|
|
@@ -6647,7 +5379,6 @@ function mapStartupError(layer, operation) {
|
|
|
6647
5379
|
|
|
6648
5380
|
//#endregion
|
|
6649
5381
|
//#region ../platform-node/src/Installation.ts
|
|
6650
|
-
/** Starts auth, persistence, Studio realtime, delivery, health, and maintenance as one deep module. */
|
|
6651
5382
|
const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
|
|
6652
5383
|
return yield* startInstallation({
|
|
6653
5384
|
cleanup: options.bootstrap.media.enabled ? options.cleanup : void 0,
|
|
@@ -6656,272 +5387,17 @@ const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
|
|
|
6656
5387
|
});
|
|
6657
5388
|
function createCmsInstallationLayer(options) {
|
|
6658
5389
|
return installationLayer({
|
|
5390
|
+
baseUrl: options.baseUrl,
|
|
5391
|
+
formHandlers: options.formHandlers,
|
|
6659
5392
|
authoring: options.authoring,
|
|
6660
5393
|
bootstrap: options.bootstrap,
|
|
6661
5394
|
composition: options.composition,
|
|
6662
5395
|
studioSecret: options.studioSecret,
|
|
6663
5396
|
documentModel: options.documentModel,
|
|
6664
|
-
initialSources: options.initialSources,
|
|
6665
5397
|
initializationToken: options.initializationToken
|
|
6666
5398
|
});
|
|
6667
5399
|
}
|
|
6668
5400
|
|
|
6669
|
-
//#endregion
|
|
6670
|
-
//#region ../s3/src/internal/client.ts
|
|
6671
|
-
var S3RequestError = class extends Schema.TaggedError()("S3.RequestError", {
|
|
6672
|
-
cause: Schema.Defect(),
|
|
6673
|
-
operation: Schema.NonEmptyString
|
|
6674
|
-
}) {};
|
|
6675
|
-
function openAwsS3MediaObjectClient(options) {
|
|
6676
|
-
const credentials = options.credentials ? {
|
|
6677
|
-
accessKeyId: options.credentials.accessKeyId,
|
|
6678
|
-
secretAccessKey: Redacted.value(options.credentials.secretAccessKey),
|
|
6679
|
-
sessionToken: options.credentials.sessionToken ? Redacted.value(options.credentials.sessionToken) : void 0
|
|
6680
|
-
} : void 0;
|
|
6681
|
-
return makeAwsS3MediaObjectClient(new S3Client({
|
|
6682
|
-
credentials,
|
|
6683
|
-
endpoint: options.endpoint,
|
|
6684
|
-
forcePathStyle: options.forcePathStyle,
|
|
6685
|
-
region: options.region
|
|
6686
|
-
}));
|
|
6687
|
-
}
|
|
6688
|
-
/** Wraps the official client; exported only through the package-private path for transport-level tests. */
|
|
6689
|
-
function makeAwsS3MediaObjectClient(client) {
|
|
6690
|
-
const request = (operation, send) => Effect.tryPromise({
|
|
6691
|
-
catch: (cause) => new S3RequestError({
|
|
6692
|
-
cause,
|
|
6693
|
-
operation
|
|
6694
|
-
}),
|
|
6695
|
-
try: send
|
|
6696
|
-
});
|
|
6697
|
-
return {
|
|
6698
|
-
close: Effect.sync(() => client.destroy()),
|
|
6699
|
-
copyObject: (input) => request("MediaObjectStore.S3.copyObject", (signal) => client.send(new CopyObjectCommand({
|
|
6700
|
-
Bucket: input.bucket,
|
|
6701
|
-
CacheControl: input.cacheControl,
|
|
6702
|
-
ContentType: input.contentType,
|
|
6703
|
-
CopySource: input.copySource,
|
|
6704
|
-
Key: input.key,
|
|
6705
|
-
MetadataDirective: "REPLACE"
|
|
6706
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6707
|
-
createBucket: (input) => request("MediaObjectStore.S3.createBucket", (signal) => client.send(new CreateBucketCommand({
|
|
6708
|
-
Bucket: input.bucket,
|
|
6709
|
-
CreateBucketConfiguration: input.locationConstraint ? { LocationConstraint: input.locationConstraint } : void 0
|
|
6710
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6711
|
-
deleteObject: (input) => request("MediaObjectStore.S3.deleteObject", (signal) => client.send(new DeleteObjectCommand({
|
|
6712
|
-
Bucket: input.bucket,
|
|
6713
|
-
Key: input.key
|
|
6714
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6715
|
-
headBucket: (input) => request("MediaObjectStore.S3.headBucket", (signal) => client.send(new HeadBucketCommand({ Bucket: input.bucket }), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6716
|
-
headObject: (input) => request("MediaObjectStore.S3.headObject", (signal) => client.send(new HeadObjectCommand({
|
|
6717
|
-
Bucket: input.bucket,
|
|
6718
|
-
Key: input.key
|
|
6719
|
-
}), { abortSignal: signal })).pipe(Effect.map((output) => ({
|
|
6720
|
-
contentLength: output.ContentLength,
|
|
6721
|
-
contentType: output.ContentType
|
|
6722
|
-
}))),
|
|
6723
|
-
listObjects: (input) => request("MediaObjectStore.S3.listObjects", (signal) => client.send(new ListObjectsV2Command({
|
|
6724
|
-
Bucket: input.bucket,
|
|
6725
|
-
ContinuationToken: input.continuationToken,
|
|
6726
|
-
Prefix: input.prefix
|
|
6727
|
-
}), { abortSignal: signal })).pipe(Effect.map((output) => ({
|
|
6728
|
-
isTruncated: output.IsTruncated === true,
|
|
6729
|
-
nextContinuationToken: output.NextContinuationToken,
|
|
6730
|
-
objects: (output.Contents ?? []).map((object) => ({
|
|
6731
|
-
key: object.Key,
|
|
6732
|
-
size: object.Size
|
|
6733
|
-
}))
|
|
6734
|
-
}))),
|
|
6735
|
-
putObject: (input) => request("MediaObjectStore.S3.putObject", (signal) => client.send(new PutObjectCommand({
|
|
6736
|
-
Body: NodeStream.toReadableNever(Stream.fromAsyncIterable(input.body, (cause) => cause)),
|
|
6737
|
-
Bucket: input.bucket,
|
|
6738
|
-
CacheControl: input.cacheControl,
|
|
6739
|
-
ContentLength: input.contentLength,
|
|
6740
|
-
ContentType: input.contentType,
|
|
6741
|
-
Key: input.key
|
|
6742
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid)
|
|
6743
|
-
};
|
|
6744
|
-
}
|
|
6745
|
-
|
|
6746
|
-
//#endregion
|
|
6747
|
-
//#region ../s3/src/internal/media-object-store.ts
|
|
6748
|
-
const mediaPrefix = "media/";
|
|
6749
|
-
const replaceableCacheControl = "public, max-age=0, must-revalidate";
|
|
6750
|
-
const isWriteBodyError = Schema.is(Schema.Union([MediaInvalid, MediaStorageError]));
|
|
6751
|
-
/** Owns and closes the official AWS client for the lifetime of the layer. */
|
|
6752
|
-
function s3MediaObjectStoreLayer(options) {
|
|
6753
|
-
return Layer.effect(MediaObjectStore, resolveOptions(options).pipe(Effect.flatMap((resolved) => Effect.acquireRelease(Effect.sync(() => openAwsS3MediaObjectClient(resolved)), (client) => client.close).pipe(Effect.tap((client) => provisionBucket(resolved, client)), Effect.map((client) => makeStore(resolved, client))))));
|
|
6754
|
-
}
|
|
6755
|
-
function makeStore(options, client) {
|
|
6756
|
-
return {
|
|
6757
|
-
list: listObjects(options.bucket, client),
|
|
6758
|
-
promote: (input) => promoteObject(options.bucket, input, client),
|
|
6759
|
-
put: (input) => putObject(options.bucket, input, client),
|
|
6760
|
-
remove: (objectKey) => providerRequest("MediaObjectStore.S3.remove", client.deleteObject({
|
|
6761
|
-
bucket: options.bucket,
|
|
6762
|
-
key: objectKey
|
|
6763
|
-
})),
|
|
6764
|
-
stat: (objectKey) => statObject(options.bucket, objectKey, client)
|
|
6765
|
-
};
|
|
6766
|
-
}
|
|
6767
|
-
function promoteObject(bucket, input, client) {
|
|
6768
|
-
return statObject(bucket, input.sourceObjectKey, client).pipe(Effect.flatMap((source) => Option.isSome(source) && source.value.byteSize === input.byteSize && source.value.mimeType === input.mimeType ? Effect.void : Effect.fail(storageError("MediaObjectStore.S3.promote.source", "The staged object metadata does not match its intent."))), Effect.flatMap(() => providerRequest("MediaObjectStore.S3.promote.copy", client.copyObject({
|
|
6769
|
-
bucket,
|
|
6770
|
-
cacheControl: replaceableCacheControl,
|
|
6771
|
-
contentType: input.mimeType,
|
|
6772
|
-
copySource: copySource(bucket, input.sourceObjectKey),
|
|
6773
|
-
key: input.destinationObjectKey
|
|
6774
|
-
}))), Effect.flatMap(() => statObject(bucket, input.destinationObjectKey, client)), Effect.flatMap((destination) => Option.isSome(destination) && destination.value.byteSize === input.byteSize && destination.value.mimeType === input.mimeType ? Effect.succeed(destination.value) : Effect.fail(storageError("MediaObjectStore.S3.promote.verify", "The promoted object metadata does not match its intent."))));
|
|
6775
|
-
}
|
|
6776
|
-
function copySource(bucket, objectKey) {
|
|
6777
|
-
return `/${encodeURIComponent(bucket)}/${objectKey.split("/").map(encodeURIComponent).join("/")}`;
|
|
6778
|
-
}
|
|
6779
|
-
function putObject(bucket, input, client) {
|
|
6780
|
-
return Effect.gen(function* () {
|
|
6781
|
-
const body = yield* validatedWriteBody(input);
|
|
6782
|
-
const iterable = yield* Stream.toAsyncIterableEffect(body);
|
|
6783
|
-
yield* client.putObject({
|
|
6784
|
-
body: iterable,
|
|
6785
|
-
bucket,
|
|
6786
|
-
cacheControl: replaceableCacheControl,
|
|
6787
|
-
contentLength: input.byteSize,
|
|
6788
|
-
contentType: input.mimeType,
|
|
6789
|
-
key: input.objectKey
|
|
6790
|
-
}).pipe(Effect.catch((failure) => isWriteBodyError(failure.cause) ? Effect.fail(failure.cause) : Effect.fail(providerError("MediaObjectStore.S3.put", failure))));
|
|
6791
|
-
return {
|
|
6792
|
-
byteSize: input.byteSize,
|
|
6793
|
-
mimeType: input.mimeType,
|
|
6794
|
-
objectKey: input.objectKey
|
|
6795
|
-
};
|
|
6796
|
-
});
|
|
6797
|
-
}
|
|
6798
|
-
const validatedWriteBody = Effect.fn("MediaObjectStore.S3.validateWriteBody")(function* (input) {
|
|
6799
|
-
const byteLength = yield* Ref.make(0);
|
|
6800
|
-
return input.body.pipe(Stream.mapEffect((chunk) => Effect.gen(function* () {
|
|
6801
|
-
if ((yield* Ref.updateAndGet(byteLength, (current) => current + chunk.byteLength)) > input.byteSize) return yield* storageError("MediaObjectStore.S3.put.body", "The object body exceeds its declared byte size.");
|
|
6802
|
-
return chunk;
|
|
6803
|
-
})), Stream.concat(Stream.fromEffect(Ref.get(byteLength).pipe(Effect.flatMap((actual) => actual === input.byteSize ? Effect.void : Effect.fail(storageError("MediaObjectStore.S3.put.body", "The object body does not match its declared byte size."))))).pipe(Stream.drain)));
|
|
6804
|
-
});
|
|
6805
|
-
function statObject(bucket, objectKey, client) {
|
|
6806
|
-
return client.headObject({
|
|
6807
|
-
bucket,
|
|
6808
|
-
key: objectKey
|
|
6809
|
-
}).pipe(Effect.matchEffect({
|
|
6810
|
-
onFailure: (failure) => isMissingObject(failure.cause) ? Effect.succeed(Option.none()) : Effect.fail(providerError("MediaObjectStore.S3.stat", failure)),
|
|
6811
|
-
onSuccess: (output) => decodeStoredObject("MediaObjectStore.S3.stat", objectKey, output.contentLength, output.contentType).pipe(Effect.asSome)
|
|
6812
|
-
}));
|
|
6813
|
-
}
|
|
6814
|
-
function listObjects(bucket, client) {
|
|
6815
|
-
return Stream.paginate({ seenTokens: /* @__PURE__ */ new Set() }, (state) => providerRequest("MediaObjectStore.S3.list", client.listObjects({
|
|
6816
|
-
bucket,
|
|
6817
|
-
continuationToken: state.continuationToken,
|
|
6818
|
-
prefix: mediaPrefix
|
|
6819
|
-
})).pipe(Effect.flatMap((page) => Effect.gen(function* () {
|
|
6820
|
-
return [yield* Effect.forEach(page.objects, decodeListedObject), yield* nextListState(state, page)];
|
|
6821
|
-
}))));
|
|
6822
|
-
}
|
|
6823
|
-
function decodeListedObject(object) {
|
|
6824
|
-
if (object.key === void 0) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned object metadata without a key."));
|
|
6825
|
-
return decodeStoredObject("MediaObjectStore.S3.list", object.key, object.size);
|
|
6826
|
-
}
|
|
6827
|
-
function nextListState(state, page) {
|
|
6828
|
-
if (!page.isTruncated) return Effect.succeedNone;
|
|
6829
|
-
const token = page.nextContinuationToken;
|
|
6830
|
-
if (!token || token === state.continuationToken || state.seenTokens.has(token)) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned an invalid continuation token."));
|
|
6831
|
-
return Effect.succeedSome({
|
|
6832
|
-
continuationToken: token,
|
|
6833
|
-
seenTokens: /* @__PURE__ */ new Set([...state.seenTokens, token])
|
|
6834
|
-
});
|
|
6835
|
-
}
|
|
6836
|
-
function decodeStoredObject(operation, objectKey, byteSize, contentType) {
|
|
6837
|
-
const decodedObjectKey = Schema.decodeOption(MediaObjectKey)(objectKey);
|
|
6838
|
-
const decodedByteSize = Schema.decodeUnknownOption(MediaByteSize)(byteSize);
|
|
6839
|
-
if (Option.isNone(decodedObjectKey) || Option.isNone(decodedByteSize)) return Effect.fail(storageError(operation, "The provider returned invalid object metadata."));
|
|
6840
|
-
const decodedMimeType = Schema.decodeUnknownOption(ImageMimeType)(contentType);
|
|
6841
|
-
const storedObject = {
|
|
6842
|
-
byteSize: decodedByteSize.value,
|
|
6843
|
-
objectKey: decodedObjectKey.value
|
|
6844
|
-
};
|
|
6845
|
-
return Effect.succeed(Option.isSome(decodedMimeType) ? {
|
|
6846
|
-
...storedObject,
|
|
6847
|
-
mimeType: decodedMimeType.value
|
|
6848
|
-
} : storedObject);
|
|
6849
|
-
}
|
|
6850
|
-
function providerRequest(operation, request) {
|
|
6851
|
-
return request.pipe(Effect.mapError((failure) => providerError(operation, failure)));
|
|
6852
|
-
}
|
|
6853
|
-
function provisionBucket(options, client) {
|
|
6854
|
-
if (!options.provisionBucket) return Effect.void;
|
|
6855
|
-
return client.headBucket({ bucket: options.bucket }).pipe(Effect.catch((failure) => isMissingBucket(failure.cause) ? createMissingBucket(options, client) : Effect.fail(providerError("MediaObjectStore.S3.bucket.head", failure))));
|
|
6856
|
-
}
|
|
6857
|
-
function createMissingBucket(options, client) {
|
|
6858
|
-
return client.createBucket({
|
|
6859
|
-
bucket: options.bucket,
|
|
6860
|
-
locationConstraint: options.endpoint || options.region === "us-east-1" ? void 0 : options.region
|
|
6861
|
-
}).pipe(Effect.catch((failure) => isBucketAlreadyOwned(failure.cause) ? providerRequest("MediaObjectStore.S3.bucket.verify", client.headBucket({ bucket: options.bucket })) : Effect.fail(providerError("MediaObjectStore.S3.bucket.create", failure))));
|
|
6862
|
-
}
|
|
6863
|
-
const ProviderFailureDetails = Schema.Struct({
|
|
6864
|
-
$metadata: Schema.optionalKey(Schema.Struct({
|
|
6865
|
-
attempts: Schema.optionalKey(Schema.Finite),
|
|
6866
|
-
httpStatusCode: Schema.optionalKey(Schema.Finite)
|
|
6867
|
-
})),
|
|
6868
|
-
name: Schema.optionalKey(Schema.String)
|
|
6869
|
-
});
|
|
6870
|
-
const decodeProviderFailure = Schema.decodeUnknownOption(ProviderFailureDetails);
|
|
6871
|
-
function providerError(operation, failure) {
|
|
6872
|
-
const details = decodeProviderFailure(failure.cause);
|
|
6873
|
-
const diagnostics = [];
|
|
6874
|
-
if (Option.isSome(details)) {
|
|
6875
|
-
if (details.value.name) diagnostics.push(`name=${details.value.name}`);
|
|
6876
|
-
if (details.value.$metadata?.httpStatusCode !== void 0) diagnostics.push(`status=${details.value.$metadata.httpStatusCode}`);
|
|
6877
|
-
if (details.value.$metadata?.attempts !== void 0) diagnostics.push(`attempts=${details.value.$metadata.attempts}`);
|
|
6878
|
-
}
|
|
6879
|
-
return storageError(operation, `The S3 provider request failed${diagnostics.length > 0 ? ` (${diagnostics.join(", ")}).` : "."}`);
|
|
6880
|
-
}
|
|
6881
|
-
function isMissingObject(cause) {
|
|
6882
|
-
const details = decodeProviderFailure(cause);
|
|
6883
|
-
if (Option.isNone(details)) return false;
|
|
6884
|
-
return details.value.name === "NoSuchKey" || details.value.name === "NotFound";
|
|
6885
|
-
}
|
|
6886
|
-
function isMissingBucket(cause) {
|
|
6887
|
-
const details = decodeProviderFailure(cause);
|
|
6888
|
-
if (Option.isNone(details)) return false;
|
|
6889
|
-
return details.value.name === "NoSuchBucket" || details.value.$metadata?.httpStatusCode === 404;
|
|
6890
|
-
}
|
|
6891
|
-
function isBucketAlreadyOwned(cause) {
|
|
6892
|
-
const details = decodeProviderFailure(cause);
|
|
6893
|
-
return Option.isSome(details) && details.value.name === "BucketAlreadyOwnedByYou";
|
|
6894
|
-
}
|
|
6895
|
-
function resolveOptions(options) {
|
|
6896
|
-
const bucket = options.bucket.trim();
|
|
6897
|
-
const region = options.region.trim();
|
|
6898
|
-
const endpoint = options.endpoint?.trim();
|
|
6899
|
-
if (!bucket || !region || options.endpoint !== void 0 && !endpoint) return Effect.fail(storageError("MediaObjectStore.S3.configure", "The bucket, region, and configured endpoint must be non-empty."));
|
|
6900
|
-
if (options.credentials) {
|
|
6901
|
-
const secretAccessKey = Redacted.value(options.credentials.secretAccessKey);
|
|
6902
|
-
const sessionToken = options.credentials.sessionToken && Redacted.value(options.credentials.sessionToken);
|
|
6903
|
-
if (!options.credentials.accessKeyId.trim() || !secretAccessKey || sessionToken === "") return Effect.fail(storageError("MediaObjectStore.S3.configure", "The configured credentials must be non-empty."));
|
|
6904
|
-
}
|
|
6905
|
-
return Effect.succeed({
|
|
6906
|
-
...options,
|
|
6907
|
-
bucket,
|
|
6908
|
-
credentials: options.credentials ? {
|
|
6909
|
-
...options.credentials,
|
|
6910
|
-
accessKeyId: options.credentials.accessKeyId.trim()
|
|
6911
|
-
} : void 0,
|
|
6912
|
-
endpoint,
|
|
6913
|
-
forcePathStyle: options.forcePathStyle ?? false,
|
|
6914
|
-
provisionBucket: options.provisionBucket ?? false,
|
|
6915
|
-
region
|
|
6916
|
-
});
|
|
6917
|
-
}
|
|
6918
|
-
function storageError(operation, message) {
|
|
6919
|
-
return new MediaStorageError({
|
|
6920
|
-
cause: new Error(message),
|
|
6921
|
-
operation
|
|
6922
|
-
});
|
|
6923
|
-
}
|
|
6924
|
-
|
|
6925
5401
|
//#endregion
|
|
6926
5402
|
//#region ../sql-pg/src/internal/database/table-helpers.ts
|
|
6927
5403
|
const transactionTimestamp = sql`transaction_timestamp()`;
|
|
@@ -6935,6 +5411,7 @@ const ACTIVITY_TARGET_KINDS = [
|
|
|
6935
5411
|
"document",
|
|
6936
5412
|
"media",
|
|
6937
5413
|
"media-library",
|
|
5414
|
+
"seo",
|
|
6938
5415
|
"user"
|
|
6939
5416
|
];
|
|
6940
5417
|
const IDENTIFIED_ACTIVITY_TARGET_KINDS = [
|
|
@@ -6961,8 +5438,8 @@ const activityEntries = pgTable("activity_entries", {
|
|
|
6961
5438
|
}, (table) => [
|
|
6962
5439
|
check("activity_entries_action_check", isOneOf(table.action, ACTIVITY_ACTIONS)),
|
|
6963
5440
|
check("activity_entries_target_kind_check", isOneOf(table.targetKind, ACTIVITY_TARGET_KINDS)),
|
|
6964
|
-
check("activity_entries_target_check", sql`(${table.targetKind}
|
|
6965
|
-
OR (${table.targetKind}
|
|
5441
|
+
check("activity_entries_target_check", sql`(${table.targetKind} in ('media-library', 'seo') AND ${table.targetId} IS NULL)
|
|
5442
|
+
OR (${table.targetKind} not in ('media-library', 'seo') AND ${table.targetId} IS NOT NULL)`),
|
|
6966
5443
|
check("activity_entries_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
|
|
6967
5444
|
check("activity_entries_details_check", sql`jsonb_typeof(${table.details}) = 'object'`),
|
|
6968
5445
|
index("activity_entries_history").on(table.occurredAt.desc(), table.id.desc()),
|
|
@@ -7053,27 +5530,30 @@ function activityEntryValues(entry) {
|
|
|
7053
5530
|
};
|
|
7054
5531
|
}
|
|
7055
5532
|
function activityEntriesEqual(left, right) {
|
|
7056
|
-
return isDeepStrictEqual(Schema.
|
|
5533
|
+
return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ActivityEntry)(left)), Result.getOrThrow(Schema.encodeResult(ActivityEntry)(right)));
|
|
7057
5534
|
}
|
|
7058
5535
|
function decodeActivityTarget(kind, id) {
|
|
7059
|
-
|
|
5536
|
+
if (kind === "document") return Schema.decodeUnknownEffect(ActivityTarget)({
|
|
7060
5537
|
documentId: id,
|
|
7061
5538
|
kind
|
|
7062
|
-
}
|
|
5539
|
+
});
|
|
5540
|
+
if (kind === "media") return Schema.decodeUnknownEffect(ActivityTarget)({
|
|
7063
5541
|
kind,
|
|
7064
5542
|
mediaId: id
|
|
7065
|
-
}
|
|
5543
|
+
});
|
|
5544
|
+
if (kind === "user") return Schema.decodeUnknownEffect(ActivityTarget)({
|
|
7066
5545
|
kind,
|
|
7067
5546
|
userId: id
|
|
7068
|
-
}
|
|
7069
|
-
return Schema.
|
|
5547
|
+
});
|
|
5548
|
+
return Schema.decodeEffect(ActivityTarget)({ kind });
|
|
7070
5549
|
}
|
|
7071
5550
|
function activityTargetId(entry) {
|
|
7072
5551
|
switch (entry.target.kind) {
|
|
7073
5552
|
case "document": return entry.target.documentId;
|
|
7074
5553
|
case "media": return entry.target.mediaId;
|
|
7075
5554
|
case "user": return entry.target.userId;
|
|
7076
|
-
case "media-library":
|
|
5555
|
+
case "media-library":
|
|
5556
|
+
case "seo": return;
|
|
7077
5557
|
}
|
|
7078
5558
|
}
|
|
7079
5559
|
function activityPage(entries, limit, total) {
|
|
@@ -7372,7 +5852,8 @@ function targetColumns(target) {
|
|
|
7372
5852
|
targetId: target.userId,
|
|
7373
5853
|
targetKind: target.kind
|
|
7374
5854
|
};
|
|
7375
|
-
case "media-library":
|
|
5855
|
+
case "media-library":
|
|
5856
|
+
case "seo": return { targetKind: target.kind };
|
|
7376
5857
|
}
|
|
7377
5858
|
}
|
|
7378
5859
|
function decodeOptionalTarget(kind, id) {
|
|
@@ -7380,7 +5861,7 @@ function decodeOptionalTarget(kind, id) {
|
|
|
7380
5861
|
return decodeActivityTarget(kind, id).pipe(Effect.mapError(() => new ChangeMismatch({})));
|
|
7381
5862
|
}
|
|
7382
5863
|
function draftEqual(change, draft) {
|
|
7383
|
-
return isDeepStrictEqual(Schema.
|
|
5864
|
+
return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draftFromChange(change))), Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draft)));
|
|
7384
5865
|
}
|
|
7385
5866
|
function draftFromChange(change) {
|
|
7386
5867
|
const value = {
|
|
@@ -7397,7 +5878,7 @@ function draftFromChange(change) {
|
|
|
7397
5878
|
});
|
|
7398
5879
|
}
|
|
7399
5880
|
function changeEqual(left, right) {
|
|
7400
|
-
return isDeepStrictEqual(Schema.
|
|
5881
|
+
return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(PendingChange)(left)), Result.getOrThrow(Schema.encodeResult(PendingChange)(right)));
|
|
7401
5882
|
}
|
|
7402
5883
|
function boundedLimit(limit) {
|
|
7403
5884
|
return Math.min(100, Math.max(0, Math.trunc(limit)));
|
|
@@ -7505,6 +5986,26 @@ const embeddedMigrations = [
|
|
|
7505
5986
|
folderMillis: 1788866265e3,
|
|
7506
5987
|
hash: "24c56a37796caf8d76c0a97128a382b31e71808aa5b0ca7e5bac96795f9e16c7",
|
|
7507
5988
|
name: "20260908111745_classy_johnny_storm"
|
|
5989
|
+
},
|
|
5990
|
+
{
|
|
5991
|
+
sql: [
|
|
5992
|
+
"CREATE TABLE \"seo_media_references\" (\n \"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n \"scope\" text NOT NULL,\n \"media_id\" uuid NOT NULL,\n CONSTRAINT \"seo_media_references_scope_media_unique\" UNIQUE(\"scope\",\"media_id\"),\n CONSTRAINT \"seo_media_references_scope_check\" CHECK (\"scope\" in ('working', 'published'))\n);\n",
|
|
5993
|
+
"\nCREATE TABLE \"seo_revisions\" (\n \"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n \"settings\" jsonb NOT NULL,\n \"actor_id\" text NOT NULL,\n \"actor_email\" text NOT NULL,\n \"actor_name\" text NOT NULL,\n \"actor_roles\" jsonb NOT NULL,\n \"request_id\" uuid NOT NULL,\n \"reason\" text,\n \"created_at\" timestamp with time zone DEFAULT transaction_timestamp() NOT NULL,\n CONSTRAINT \"seo_revisions_actor_roles_check\" CHECK (jsonb_typeof(\"actor_roles\") = 'array'),\n CONSTRAINT \"seo_revisions_settings_check\" CHECK (jsonb_typeof(\"settings\") = 'object')\n);\n",
|
|
5994
|
+
"\nCREATE TABLE \"seo_state\" (\n \"singleton\" boolean PRIMARY KEY DEFAULT true,\n \"working_revision_id\" uuid NOT NULL,\n \"published_revision_id\" uuid NOT NULL,\n CONSTRAINT \"seo_state_singleton_check\" CHECK (\"singleton\")\n);\n",
|
|
5995
|
+
"\nCREATE INDEX \"seo_media_references_media\" ON \"seo_media_references\" (\"media_id\");",
|
|
5996
|
+
"\nCREATE INDEX \"seo_revisions_history\" ON \"seo_revisions\" (\"created_at\" DESC NULLS LAST,\"id\" DESC NULLS LAST);",
|
|
5997
|
+
"\nALTER TABLE \"seo_media_references\" ADD CONSTRAINT \"seo_media_references_media_id_media_assets_id_fkey\" FOREIGN KEY (\"media_id\") REFERENCES \"media_assets\"(\"id\") ON DELETE RESTRICT;",
|
|
5998
|
+
"\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_working_revision_fkey\" FOREIGN KEY (\"working_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
|
|
5999
|
+
"\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_published_revision_fkey\" FOREIGN KEY (\"published_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
|
|
6000
|
+
"\nALTER TABLE \"activity_entries\" DROP CONSTRAINT \"activity_entries_action_check\", ADD CONSTRAINT \"activity_entries_action_check\" CHECK (\"action\" in ('document.created', 'document.updated', 'document.moved', 'document.trashed', 'document.revision.restored', 'document.untrashed', 'document.published', 'document.unpublished', 'media.uploaded', 'media.replaced', 'media.removed', 'media.cleaned', 'seo.saved', 'seo.published', 'user.created', 'user.removed', 'user.role.changed', 'user.sessions.revoked', 'authentication.signed-in', 'authentication.signed-out', 'authentication.password.changed', 'authentication.password.reset'));",
|
|
6001
|
+
"\nALTER TABLE \"activity_entries\" DROP CONSTRAINT \"activity_entries_target_kind_check\", ADD CONSTRAINT \"activity_entries_target_kind_check\" CHECK (\"target_kind\" in ('document', 'media', 'media-library', 'seo', 'user'));",
|
|
6002
|
+
"\nALTER TABLE \"activity_entries\" DROP CONSTRAINT \"activity_entries_target_check\", ADD CONSTRAINT \"activity_entries_target_check\" CHECK ((\"target_kind\" in ('media-library', 'seo') AND \"target_id\" IS NULL)\n OR (\"target_kind\" not in ('media-library', 'seo') AND \"target_id\" IS NOT NULL));",
|
|
6003
|
+
"\nALTER TABLE \"pending_changes\" DROP CONSTRAINT \"pending_changes_action_check\", ADD CONSTRAINT \"pending_changes_action_check\" CHECK (\"action\" in ('document.created', 'document.updated', 'document.moved', 'document.trashed', 'document.revision.restored', 'document.untrashed', 'document.published', 'document.unpublished', 'media.uploaded', 'media.replaced', 'media.removed', 'media.cleaned', 'seo.saved', 'seo.published', 'user.created', 'user.removed', 'user.role.changed', 'user.sessions.revoked', 'authentication.signed-in', 'authentication.signed-out', 'authentication.password.changed', 'authentication.password.reset'));"
|
|
6004
|
+
],
|
|
6005
|
+
bps: true,
|
|
6006
|
+
folderMillis: 1789233351e3,
|
|
6007
|
+
hash: "8e6a6abf5385ab3ecf8eb083c1923ac9c74d72aa71bd1d444b7402d3e7e77000",
|
|
6008
|
+
name: "20260912171551_charming_valkyrie"
|
|
7508
6009
|
}
|
|
7509
6010
|
];
|
|
7510
6011
|
|
|
@@ -7603,12 +6104,16 @@ const initializationState = pgTable("initialization_state", {
|
|
|
7603
6104
|
|
|
7604
6105
|
//#endregion
|
|
7605
6106
|
//#region ../sql-pg/src/internal/database/migrations.ts
|
|
7606
|
-
|
|
6107
|
+
const MIGRATION_LOCK_KEY = "cms:database-migrations:v1";
|
|
7607
6108
|
const migrateCmsDatabase = Effect.fn("CmsDatabase.migrate")(function* () {
|
|
6109
|
+
const postgres = yield* PgClient.PgClient;
|
|
7608
6110
|
const database = yield* PgDrizzle.makeWithDefaults();
|
|
7609
|
-
yield*
|
|
7610
|
-
|
|
7611
|
-
|
|
6111
|
+
yield* postgres.withTransaction(Effect.gen(function* () {
|
|
6112
|
+
yield* postgres.unsafe("select pg_advisory_xact_lock(hashtextextended($1, 0))", [MIGRATION_LOCK_KEY]);
|
|
6113
|
+
yield* migrate(embeddedMigrations, database._.session, "__drizzle_migrations");
|
|
6114
|
+
yield* database.insert(cmsState).values({}).onConflictDoNothing({ target: cmsState.singleton });
|
|
6115
|
+
yield* database.insert(initializationState).values({}).onConflictDoNothing({ target: initializationState.singleton });
|
|
6116
|
+
}));
|
|
7612
6117
|
});
|
|
7613
6118
|
const cmsDatabaseMigrationLayer = Layer.effectDiscard(migrateCmsDatabase());
|
|
7614
6119
|
|
|
@@ -7625,13 +6130,15 @@ const applyDocumentCommitPlan = Effect.fn("DocumentStore.Drizzle.applyCommitPlan
|
|
|
7625
6130
|
const revisionId = yield* appendRevision(transaction, plan, activity);
|
|
7626
6131
|
yield* transaction.insert(documentHeads).values({
|
|
7627
6132
|
authoredRevisionId: revisionId,
|
|
7628
|
-
documentId: plan.documentId
|
|
6133
|
+
documentId: plan.documentId,
|
|
6134
|
+
publishedRevisionId: plan.publish ? revisionId : null
|
|
7629
6135
|
});
|
|
7630
6136
|
return;
|
|
7631
6137
|
}
|
|
7632
6138
|
case "document.update": {
|
|
7633
6139
|
const revisionId = yield* appendRevision(transaction, plan, activity);
|
|
7634
6140
|
yield* updateAuthoredHead(transaction, plan.documentId, revisionId);
|
|
6141
|
+
if (plan.publish) yield* transaction.update(documentHeads).set({ publishedRevisionId: revisionId }).where(eq(documentHeads.documentId, plan.documentId));
|
|
7635
6142
|
yield* touchDocument(transaction, plan.documentId);
|
|
7636
6143
|
return;
|
|
7637
6144
|
}
|
|
@@ -7712,7 +6219,7 @@ function mapActivePathConflict(sourcePath) {
|
|
|
7712
6219
|
const causeError = Cause$1.findErrorOption(error.cause);
|
|
7713
6220
|
if (Option$1.isNone(causeError) || !SqlError.isSqlError(causeError.value)) return error;
|
|
7714
6221
|
const reason = causeError.value.reason;
|
|
7715
|
-
return
|
|
6222
|
+
return Predicate.isTagged("UniqueViolation")(reason) && reason.constraint === "documents_active_path" ? new DocumentPathConflict({ sourcePath }) : error;
|
|
7716
6223
|
}));
|
|
7717
6224
|
}
|
|
7718
6225
|
function updateAuthoredHead(transaction, documentId, revisionId) {
|
|
@@ -8026,7 +6533,7 @@ const isRepositoryDomainError = Schema.is(RepositoryDomainError);
|
|
|
8026
6533
|
const isDocumentNotFound = Schema.is(DocumentNotFound);
|
|
8027
6534
|
const isDocumentMediaUnavailable = Schema.is(DocumentMediaUnavailable);
|
|
8028
6535
|
const isDocumentPersistenceError = Schema.is(DocumentPersistenceError);
|
|
8029
|
-
const systemActivity = ActivityContext.make({
|
|
6536
|
+
const systemActivity$1 = ActivityContext.make({
|
|
8030
6537
|
actor: {
|
|
8031
6538
|
email: "system@cms.invalid",
|
|
8032
6539
|
id: "system",
|
|
@@ -8049,15 +6556,15 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
8049
6556
|
const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
|
|
8050
6557
|
const plans = [input.plan, ...input.additionalPlans ?? []];
|
|
8051
6558
|
const references = plans.flatMap((plan) => input.mediaReferences.filter((reference) => reference.documentId === plan.documentId));
|
|
8052
|
-
if (references.length !== input.mediaReferences.length) return yield* persistenceFailure$
|
|
6559
|
+
if (references.length !== input.mediaReferences.length) return yield* persistenceFailure$3("DocumentStore.commit", /* @__PURE__ */ new Error("Unexpected media reference owner."));
|
|
8053
6560
|
yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
|
|
8054
|
-
yield*
|
|
8055
|
-
const activity = input.activity ?? systemActivity;
|
|
6561
|
+
yield* rejectUnavailableMedia(transaction, references);
|
|
6562
|
+
const activity = input.activity ?? systemActivity$1;
|
|
8056
6563
|
for (const plan of plans) {
|
|
8057
6564
|
yield* applyDocumentCommitPlan(transaction, plan, activity);
|
|
8058
6565
|
yield* replaceDocumentMediaReferences(transaction, plan.documentId, references.filter((reference) => reference.documentId === plan.documentId));
|
|
8059
6566
|
}
|
|
8060
|
-
const snapshot = yield* readSnapshot(transaction, state.generation);
|
|
6567
|
+
const snapshot = yield* readSnapshot$1(transaction, state.generation);
|
|
8061
6568
|
for (const plan of plans) {
|
|
8062
6569
|
const affected = yield* sourceForMutation(snapshot, plan.documentId, plan.action === "document.remove");
|
|
8063
6570
|
yield* recordDocumentActivity(transaction, affected, plan, activity);
|
|
@@ -8079,15 +6586,15 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
8079
6586
|
const load = Effect.fn("DocumentStore.Drizzle.load")((after) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8080
6587
|
const generation = yield* readGeneration(transaction);
|
|
8081
6588
|
if (after === generation.toString()) return void 0;
|
|
8082
|
-
return yield* readSnapshot(transaction, generation);
|
|
6589
|
+
return yield* readSnapshot$1(transaction, generation);
|
|
8083
6590
|
}), {
|
|
8084
6591
|
accessMode: "read only",
|
|
8085
6592
|
isolationLevel: "repeatable read"
|
|
8086
|
-
}).pipe(mapPersistenceFailure$
|
|
6593
|
+
}).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.load")));
|
|
8087
6594
|
const synchronizeMediaReferences = Effect.fn("DocumentStore.Drizzle.synchronizeMediaReferences")((input) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8088
6595
|
const stateRows = yield* transaction.select().from(cmsState).where(eq(cmsState.singleton, true)).limit(2).for("update");
|
|
8089
6596
|
const stateRow = stateRows[0];
|
|
8090
|
-
if (stateRows.length !== 1 || !stateRow) return yield* persistenceFailure$
|
|
6597
|
+
if (stateRows.length !== 1 || !stateRow) return yield* persistenceFailure$3("DocumentStore.Drizzle.synchronizeMediaReferences", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${stateRows.length}.`));
|
|
8091
6598
|
const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
|
|
8092
6599
|
if (state.generation.toString() !== input.expectedGeneration) return yield* new DocumentGenerationConflict({
|
|
8093
6600
|
actualGeneration: DocumentGeneration.make(String(state.generation)),
|
|
@@ -8095,7 +6602,7 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
8095
6602
|
});
|
|
8096
6603
|
const references = uniqueMediaReferences(input.references);
|
|
8097
6604
|
yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
|
|
8098
|
-
yield*
|
|
6605
|
+
yield* rejectUnavailableMedia(transaction, references);
|
|
8099
6606
|
yield* transaction.delete(documentMediaReferences);
|
|
8100
6607
|
if (references.length > 0) yield* transaction.insert(documentMediaReferences).values(references.map(referenceValues));
|
|
8101
6608
|
})).pipe(mapSynchronizationFailure("DocumentStore.Drizzle.synchronizeMediaReferences")));
|
|
@@ -8111,12 +6618,12 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
8111
6618
|
const cursor = query.cursor ? or(lt(documentRevisions.createdAt, DateTime.formatIso(query.cursor.createdAt)), and(eq(documentRevisions.createdAt, DateTime.formatIso(query.cursor.createdAt)), lt(documentRevisions.id, query.cursor.revisionId))) : void 0;
|
|
8112
6619
|
const rows = yield* transaction.select().from(documentRevisions).where(and(eq(documentRevisions.documentId, query.documentId), cursor)).orderBy(desc(documentRevisions.createdAt), desc(documentRevisions.id)).limit(query.limit + 1);
|
|
8113
6620
|
const total = (yield* transaction.select({ value: count() }).from(documentRevisions).where(eq(documentRevisions.documentId, query.documentId)))[0]?.value;
|
|
8114
|
-
if (total === void 0) return yield* persistenceFailure$
|
|
6621
|
+
if (total === void 0) return yield* persistenceFailure$3("DocumentStore.Drizzle.revisions", /* @__PURE__ */ new Error("Documents persistence did not return a history total."));
|
|
8115
6622
|
return revisionPage(yield* Effect.forEach(rows, decodeDocumentRevision), query.limit, total);
|
|
8116
6623
|
}), {
|
|
8117
6624
|
accessMode: "read only",
|
|
8118
6625
|
isolationLevel: "repeatable read"
|
|
8119
|
-
}).pipe(mapPersistenceFailure$
|
|
6626
|
+
}).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.revisions")));
|
|
8120
6627
|
return DocumentStore.of({
|
|
8121
6628
|
commit,
|
|
8122
6629
|
load,
|
|
@@ -8157,8 +6664,13 @@ const recordDocumentActivity = Effect.fn("DocumentStore.Drizzle.recordActivity")
|
|
|
8157
6664
|
};
|
|
8158
6665
|
if (plan.action !== "document.publish" && plan.action !== "document.unpublish") values.id = source.revisionId;
|
|
8159
6666
|
yield* database.insert(activityEntries).values(values);
|
|
6667
|
+
if ((plan.action === "document.create" || plan.action === "document.update") && plan.publish) yield* database.insert(activityEntries).values({
|
|
6668
|
+
...values,
|
|
6669
|
+
action: "document.published",
|
|
6670
|
+
id: void 0
|
|
6671
|
+
});
|
|
8160
6672
|
});
|
|
8161
|
-
const readSnapshot = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
|
|
6673
|
+
const readSnapshot$1 = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
|
|
8162
6674
|
const rows = yield* database.select({
|
|
8163
6675
|
authored: authoredRevision,
|
|
8164
6676
|
created: createdRevision,
|
|
@@ -8171,12 +6683,12 @@ const readSnapshot = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (
|
|
|
8171
6683
|
const readGeneration = Effect.fn("DocumentStore.Drizzle.readGeneration")(function* (database) {
|
|
8172
6684
|
const rows = yield* database.select().from(cmsState);
|
|
8173
6685
|
const row = rows[0];
|
|
8174
|
-
if (rows.length !== 1 || !row) return yield* persistenceFailure$
|
|
6686
|
+
if (rows.length !== 1 || !row) return yield* persistenceFailure$3("DocumentStore.Drizzle.readGeneration", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${rows.length}.`));
|
|
8175
6687
|
return (yield* Schema.decodeUnknownEffect(CmsStateSelect)(row)).generation;
|
|
8176
6688
|
});
|
|
8177
6689
|
const sourceForMutation = Effect.fn("DocumentStore.Drizzle.sourceForMutation")(function* (snapshot, documentId, deleted) {
|
|
8178
6690
|
const source = (deleted ? snapshot.deleted : snapshot.active).find((candidate) => candidate.documentId === documentId);
|
|
8179
|
-
if (!source) return yield* persistenceFailure$
|
|
6691
|
+
if (!source) return yield* persistenceFailure$3("DocumentStore.Drizzle.sourceForMutation", /* @__PURE__ */ new Error(`Committed document "${documentId}" is missing from its resulting snapshot.`));
|
|
8180
6692
|
return source;
|
|
8181
6693
|
});
|
|
8182
6694
|
function mapCommitFailure(operation) {
|
|
@@ -8188,14 +6700,27 @@ function mapSynchronizationFailure(operation) {
|
|
|
8188
6700
|
return documentPersistenceError(operation, cause);
|
|
8189
6701
|
}));
|
|
8190
6702
|
}
|
|
8191
|
-
const
|
|
6703
|
+
const rejectUnavailableMedia = Effect.fn("DocumentStore.Drizzle.rejectUnavailableMedia")(function* (transaction, references) {
|
|
8192
6704
|
const keys = [...new Set(references.map((reference) => reference.objectKey))];
|
|
6705
|
+
const mediaIds = [...new Set(references.flatMap((reference) => reference.mediaId ? [reference.mediaId] : []))];
|
|
8193
6706
|
if (keys.length === 0) return;
|
|
8194
|
-
|
|
8195
|
-
|
|
6707
|
+
if (mediaIds.length > 0) {
|
|
6708
|
+
const identityRows = yield* transaction.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
|
|
6709
|
+
yield* lockMediaReferenceKeys(transaction, yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(identityRows.map((row) => row.objectKey)));
|
|
6710
|
+
}
|
|
6711
|
+
const rows = yield* transaction.select({
|
|
6712
|
+
id: mediaAssets.id,
|
|
6713
|
+
objectKey: mediaAssets.objectKey,
|
|
6714
|
+
status: mediaAssets.status
|
|
6715
|
+
}).from(mediaAssets).where(mediaIds.length > 0 ? or(inArray(mediaAssets.objectKey, keys), inArray(mediaAssets.id, mediaIds)) : inArray(mediaAssets.objectKey, keys));
|
|
6716
|
+
const deletingKeys = new Set(yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(rows.filter((row) => row.status === "deleting").map((row) => row.objectKey)));
|
|
6717
|
+
const unavailable = references.filter((reference) => {
|
|
6718
|
+
if (!reference.mediaId) return deletingKeys.has(reference.objectKey);
|
|
6719
|
+
const asset = rows.find((row) => row.id === reference.mediaId);
|
|
6720
|
+
return !asset || asset.status !== "ready" || asset.objectKey !== reference.objectKey;
|
|
6721
|
+
});
|
|
8196
6722
|
if (unavailable.length === 0) return;
|
|
8197
|
-
const
|
|
8198
|
-
const affected = references.filter((reference) => unavailableSet.has(reference.objectKey)).sort((left, right) => left.documentId === right.documentId ? left.objectKey.localeCompare(right.objectKey) : left.documentId.localeCompare(right.documentId));
|
|
6723
|
+
const affected = unavailable.sort((left, right) => left.documentId === right.documentId ? left.objectKey.localeCompare(right.objectKey) : left.documentId.localeCompare(right.documentId));
|
|
8199
6724
|
const first = affected[0];
|
|
8200
6725
|
if (!first) return;
|
|
8201
6726
|
return yield* new DocumentMediaUnavailable({
|
|
@@ -8219,7 +6744,7 @@ function referenceValues(reference) {
|
|
|
8219
6744
|
sourcePath: reference.sourcePath
|
|
8220
6745
|
};
|
|
8221
6746
|
}
|
|
8222
|
-
function mapPersistenceFailure$
|
|
6747
|
+
function mapPersistenceFailure$3(operation) {
|
|
8223
6748
|
return (effect) => effect.pipe(Effect.mapError((cause) => isDocumentPersistenceError(cause) ? cause : documentPersistenceError(operation, cause)));
|
|
8224
6749
|
}
|
|
8225
6750
|
function mapRevisionFailure(operation) {
|
|
@@ -8228,7 +6753,7 @@ function mapRevisionFailure(operation) {
|
|
|
8228
6753
|
return documentPersistenceError(operation, cause);
|
|
8229
6754
|
}));
|
|
8230
6755
|
}
|
|
8231
|
-
function persistenceFailure$
|
|
6756
|
+
function persistenceFailure$3(operation, cause) {
|
|
8232
6757
|
return Effect.fail(documentPersistenceError(operation, cause));
|
|
8233
6758
|
}
|
|
8234
6759
|
|
|
@@ -8291,7 +6816,7 @@ const initializationStoreLayer = Layer.effect(InitializationStore, Effect.gen(fu
|
|
|
8291
6816
|
const locks = yield* PostgresAdvisoryLock;
|
|
8292
6817
|
const claim = Effect.fn("InitializationStore.Postgres.claim")((value) => database.update(initializationState).set({
|
|
8293
6818
|
changeId: value.changeId,
|
|
8294
|
-
claim: Schema.
|
|
6819
|
+
claim: Result.getOrThrow(Schema.encodeResult(InitializationClaim)(value)),
|
|
8295
6820
|
state: "claiming"
|
|
8296
6821
|
}).where(and(eq(initializationState.singleton, true), eq(initializationState.state, "uninitialized"))).returning({ singleton: initializationState.singleton }).pipe(Effect.map((rows) => rows.length === 1), mapUnavailable("InitializationStore.Postgres.claim")));
|
|
8297
6822
|
const complete = Effect.fn("InitializationStore.Postgres.complete")((changeId) => transition(database, changeId, "ready"));
|
|
@@ -8343,6 +6868,53 @@ function mapUnavailable(operation) {
|
|
|
8343
6868
|
})));
|
|
8344
6869
|
}
|
|
8345
6870
|
|
|
6871
|
+
//#endregion
|
|
6872
|
+
//#region ../sql-pg/src/internal/seo/tables.ts
|
|
6873
|
+
const seoRevisions = pgTable("seo_revisions", {
|
|
6874
|
+
id: uuid().defaultRandom().primaryKey(),
|
|
6875
|
+
settings: jsonb().$type().notNull(),
|
|
6876
|
+
actorId: text("actor_id").notNull(),
|
|
6877
|
+
actorEmail: text("actor_email").notNull(),
|
|
6878
|
+
actorName: text("actor_name").notNull(),
|
|
6879
|
+
actorRoles: jsonb("actor_roles").$type().notNull(),
|
|
6880
|
+
requestId: uuid("request_id").notNull(),
|
|
6881
|
+
reason: text(),
|
|
6882
|
+
createdAt: timestamp("created_at", {
|
|
6883
|
+
mode: "string",
|
|
6884
|
+
withTimezone: true
|
|
6885
|
+
}).default(transactionTimestamp).notNull()
|
|
6886
|
+
}, (table) => [
|
|
6887
|
+
check("seo_revisions_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
|
|
6888
|
+
check("seo_revisions_settings_check", sql`jsonb_typeof(${table.settings}) = 'object'`),
|
|
6889
|
+
index("seo_revisions_history").on(table.createdAt.desc(), table.id.desc())
|
|
6890
|
+
]);
|
|
6891
|
+
const seoState = pgTable("seo_state", {
|
|
6892
|
+
singleton: boolean().default(true).primaryKey(),
|
|
6893
|
+
workingRevisionId: uuid("working_revision_id").notNull(),
|
|
6894
|
+
publishedRevisionId: uuid("published_revision_id").notNull()
|
|
6895
|
+
}, (table) => [
|
|
6896
|
+
check("seo_state_singleton_check", sql`${table.singleton}`),
|
|
6897
|
+
foreignKey({
|
|
6898
|
+
columns: [table.workingRevisionId],
|
|
6899
|
+
foreignColumns: [seoRevisions.id],
|
|
6900
|
+
name: "seo_state_working_revision_fkey"
|
|
6901
|
+
}).onDelete("restrict"),
|
|
6902
|
+
foreignKey({
|
|
6903
|
+
columns: [table.publishedRevisionId],
|
|
6904
|
+
foreignColumns: [seoRevisions.id],
|
|
6905
|
+
name: "seo_state_published_revision_fkey"
|
|
6906
|
+
}).onDelete("restrict")
|
|
6907
|
+
]);
|
|
6908
|
+
const seoMediaReferences = pgTable("seo_media_references", {
|
|
6909
|
+
id: uuid().defaultRandom().primaryKey(),
|
|
6910
|
+
scope: text({ enum: ["working", "published"] }).notNull(),
|
|
6911
|
+
mediaId: uuid("media_id").notNull().references(() => mediaAssets.id, { onDelete: "restrict" })
|
|
6912
|
+
}, (table) => [
|
|
6913
|
+
check("seo_media_references_scope_check", sql`${table.scope} in ('working', 'published')`),
|
|
6914
|
+
unique("seo_media_references_scope_media_unique").on(table.scope, table.mediaId),
|
|
6915
|
+
index("seo_media_references_media").on(table.mediaId)
|
|
6916
|
+
]);
|
|
6917
|
+
|
|
8346
6918
|
//#endregion
|
|
8347
6919
|
//#region ../sql-pg/src/internal/media/row-schema.ts
|
|
8348
6920
|
const mediaRefinements = {
|
|
@@ -8410,14 +6982,14 @@ const isMediaReferenced = Schema.is(MediaReferenced);
|
|
|
8410
6982
|
const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
8411
6983
|
const database = yield* PgDrizzle.makeWithDefaults();
|
|
8412
6984
|
const reserve = Effect.fn("MediaStore.Drizzle.reserve")((record, operation) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8413
|
-
if (record.status !== "uploading") return yield* persistenceFailure$
|
|
6985
|
+
if (record.status !== "uploading") return yield* persistenceFailure$2("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`A media reservation must start as uploading, received "${record.status}".`));
|
|
8414
6986
|
yield* beginChange(transaction, mediaUploadChange(record, operation)).pipe(Effect.flatMap(requireUnresolvedChange));
|
|
8415
6987
|
const rows = yield* transaction.insert(mediaAssets).values(recordValues(record)).onConflictDoNothing().returning();
|
|
8416
6988
|
if (rows[0]) return yield* decodeMediaRecord(rows[0]);
|
|
8417
6989
|
const existing = yield* rowById(transaction, record.id);
|
|
8418
6990
|
if (existing && existing.status !== "deleting" && isSameReservation(existing, record)) return existing;
|
|
8419
|
-
return yield* persistenceFailure$
|
|
8420
|
-
})).pipe(mapPersistenceFailure$
|
|
6991
|
+
return yield* persistenceFailure$2("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`Media reservation "${record.id}" conflicts with durable media state.`));
|
|
6992
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.reserve")));
|
|
8421
6993
|
const activate = Effect.fn("MediaStore.Drizzle.activate")((id) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8422
6994
|
const existing = yield* lockedRowById(transaction, id);
|
|
8423
6995
|
if (existing?.status === "ready") return existing;
|
|
@@ -8435,11 +7007,11 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8435
7007
|
kind: "applied"
|
|
8436
7008
|
});
|
|
8437
7009
|
return activated;
|
|
8438
|
-
})).pipe(mapPersistenceFailure$
|
|
7010
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.activate")));
|
|
8439
7011
|
const find = Effect.fn("MediaStore.Drizzle.find")((id) => Effect.gen(function* () {
|
|
8440
7012
|
const rows = yield* database.select().from(mediaAssets).where(and(eq(mediaAssets.id, id), eq(mediaAssets.status, "ready"))).limit(1);
|
|
8441
7013
|
return rows[0] ? Option.some(yield* decodeMediaRecord(rows[0])) : Option.none();
|
|
8442
|
-
}).pipe(mapPersistenceFailure$
|
|
7014
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.find")));
|
|
8443
7015
|
const beginDelete = Effect.fn("MediaStore.Drizzle.beginDelete")((id, operation) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8444
7016
|
const existing = yield* lockedRowById(transaction, id);
|
|
8445
7017
|
if (!existing) return yield* new MediaNotFound({ mediaId: id });
|
|
@@ -8452,7 +7024,13 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8452
7024
|
yield* lockMediaReferenceKeys(transaction, [existing.objectKey]);
|
|
8453
7025
|
const referenceRows = yield* transaction.select().from(documentMediaReferences).where(eq(documentMediaReferences.objectKey, existing.objectKey)).orderBy(asc(documentMediaReferences.documentId), asc(documentMediaReferences.sourcePath), asc(documentMediaReferences.nodePath));
|
|
8454
7026
|
const references = yield* decodeMediaReferences(referenceRows);
|
|
8455
|
-
|
|
7027
|
+
const seoReferenceRows = yield* transaction.select({ scope: seoMediaReferences.scope }).from(seoMediaReferences).where(eq(seoMediaReferences.mediaId, id)).orderBy(asc(seoMediaReferences.scope));
|
|
7028
|
+
const seoScopes = yield* Schema.decodeEffect(Schema.Array(Schema.Literals(["working", "published"])))(seoReferenceRows.map((row) => row.scope)).pipe(Effect.mapError((cause) => mediaPersistenceError("MediaStore.Drizzle.beginDelete", cause)));
|
|
7029
|
+
if (references.length > 0 || seoScopes.length > 0) return yield* new MediaReferenced(seoScopes.length > 0 ? {
|
|
7030
|
+
mediaId: id,
|
|
7031
|
+
references,
|
|
7032
|
+
seoScopes
|
|
7033
|
+
} : {
|
|
8456
7034
|
mediaId: id,
|
|
8457
7035
|
references
|
|
8458
7036
|
});
|
|
@@ -8473,7 +7051,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8473
7051
|
const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
8474
7052
|
if (existing && isSameReplacement(existing, replacement)) return existing;
|
|
8475
7053
|
return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
8476
|
-
})).pipe(mapStoreFailure$
|
|
7054
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.beginReplace")));
|
|
8477
7055
|
const markReplaceStaged = Effect.fn("MediaStore.Drizzle.markReplaceStaged")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8478
7056
|
yield* lockedRowById(transaction, replacement.mediaId);
|
|
8479
7057
|
const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
@@ -8481,14 +7059,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8481
7059
|
if (existing.staged) return existing;
|
|
8482
7060
|
const rows = yield* transaction.update(mediaReplacements).set({ staged: true }).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning();
|
|
8483
7061
|
if (rows[0]) return yield* decodeMediaReplacement(rows[0]);
|
|
8484
|
-
return yield* persistenceFailure$
|
|
8485
|
-
})).pipe(mapStoreFailure$
|
|
7062
|
+
return yield* persistenceFailure$2("MediaStore.Drizzle.markReplaceStaged", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
|
|
7063
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.markReplaceStaged")));
|
|
8486
7064
|
const finishReplace = Effect.fn("MediaStore.Drizzle.finishReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8487
7065
|
const asset = yield* lockedRowById(transaction, replacement.mediaId);
|
|
8488
7066
|
const pendingReplacement = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
8489
7067
|
if (!pendingReplacement) {
|
|
8490
7068
|
if (asset && isCompletedReplacement(asset, replacement)) return asset;
|
|
8491
|
-
return yield* persistenceFailure$
|
|
7069
|
+
return yield* persistenceFailure$2("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" cannot be replayed from durable state.`));
|
|
8492
7070
|
}
|
|
8493
7071
|
if (!isSameReplacement(pendingReplacement, replacement)) return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
8494
7072
|
if (!asset || asset.status !== "ready" || asset.objectKey !== replacement.objectKey || asset.mimeType !== replacement.mimeType) return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
@@ -8501,7 +7079,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8501
7079
|
updatedAt: transactionTimestamp
|
|
8502
7080
|
}).where(and(eq(mediaAssets.id, replacement.mediaId), eq(mediaAssets.status, "ready"))).returning())[0];
|
|
8503
7081
|
if (!replaced) return yield* transitionFailure("MediaStore.Drizzle.finishReplace", replacement.mediaId, asset.status);
|
|
8504
|
-
if (!(yield* transaction.delete(mediaReplacements).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning({ mediaId: mediaReplacements.mediaId }))[0]) return yield* persistenceFailure$
|
|
7082
|
+
if (!(yield* transaction.delete(mediaReplacements).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning({ mediaId: mediaReplacements.mediaId }))[0]) return yield* persistenceFailure$2("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
|
|
8505
7083
|
const decoded = yield* decodeMediaRecord(replaced);
|
|
8506
7084
|
const intent = yield* decodeMediaChangeIntent(change);
|
|
8507
7085
|
yield* resolveChange(transaction, change, {
|
|
@@ -8509,7 +7087,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8509
7087
|
kind: "applied"
|
|
8510
7088
|
});
|
|
8511
7089
|
return decoded;
|
|
8512
|
-
})).pipe(mapStoreFailure$
|
|
7090
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.finishReplace")));
|
|
8513
7091
|
const cancelReplace = Effect.fn("MediaStore.Drizzle.cancelReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8514
7092
|
yield* lockedRowById(transaction, replacement.mediaId);
|
|
8515
7093
|
const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
@@ -8518,7 +7096,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8518
7096
|
const change = yield* requirePendingChange(transaction, mediaSubject(replacement.mediaId), "media.replaced");
|
|
8519
7097
|
if (!(yield* transaction.delete(mediaReplacements).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.staged, replacement.staged), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning({ mediaId: mediaReplacements.mediaId }))[0]) return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
8520
7098
|
yield* resolveChange(transaction, change, { kind: "cancelled" });
|
|
8521
|
-
})).pipe(mapStoreFailure$
|
|
7099
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.cancelReplace")));
|
|
8522
7100
|
const finishDelete = Effect.fn("MediaStore.Drizzle.finishDelete")((id) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8523
7101
|
const existing = yield* lockedRowById(transaction, id);
|
|
8524
7102
|
if (!existing) return;
|
|
@@ -8530,7 +7108,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8530
7108
|
details: mediaChangeDetails(intent),
|
|
8531
7109
|
kind: "applied"
|
|
8532
7110
|
});
|
|
8533
|
-
})).pipe(mapPersistenceFailure$
|
|
7111
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.finishDelete")));
|
|
8534
7112
|
const abandonUpload = Effect.fn("MediaStore.Drizzle.abandonUpload")((id) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8535
7113
|
const existing = yield* lockedRowById(transaction, id);
|
|
8536
7114
|
if (!existing) return;
|
|
@@ -8538,15 +7116,15 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8538
7116
|
const change = yield* requirePendingChange(transaction, mediaSubject(id), "media.uploaded");
|
|
8539
7117
|
if (!(yield* transaction.delete(mediaAssets).where(and(eq(mediaAssets.id, id), eq(mediaAssets.status, "uploading"))).returning({ id: mediaAssets.id }))[0]) return yield* transitionFailure("MediaStore.Drizzle.abandonUpload", id, existing.status);
|
|
8540
7118
|
yield* resolveChange(transaction, change, { kind: "cancelled" });
|
|
8541
|
-
})).pipe(mapPersistenceFailure$
|
|
7119
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.abandonUpload")));
|
|
8542
7120
|
const pending = Effect.fn("MediaStore.Drizzle.pending")((query) => Effect.gen(function* () {
|
|
8543
|
-
const rows = yield* database.select().from(mediaAssets).where(and(ne(mediaAssets.status, "ready"), lte(mediaAssets.updatedAt, DateTime.formatIso(query.before))
|
|
7121
|
+
const rows = yield* database.select().from(mediaAssets).where(and(ne(mediaAssets.status, "ready"), lte(mediaAssets.updatedAt, DateTime.formatIso(query.before)), query.after === void 0 ? void 0 : gt(mediaAssets.id, query.after))).orderBy(asc(mediaAssets.id)).limit(query.limit);
|
|
8544
7122
|
return yield* Effect.forEach(rows, decodeMediaRecord);
|
|
8545
|
-
}).pipe(mapPersistenceFailure$
|
|
7123
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pending")));
|
|
8546
7124
|
const pendingReplacements = Effect.fn("MediaStore.Drizzle.pendingReplacements")((query) => Effect.gen(function* () {
|
|
8547
|
-
const rows = yield* database.select().from(mediaReplacements).where(lte(mediaReplacements.updatedAt, DateTime.formatIso(query.before))
|
|
7125
|
+
const rows = yield* database.select().from(mediaReplacements).where(and(lte(mediaReplacements.updatedAt, DateTime.formatIso(query.before)), query.after === void 0 ? void 0 : gt(mediaReplacements.mediaId, query.after))).orderBy(asc(mediaReplacements.mediaId)).limit(query.limit);
|
|
8548
7126
|
return yield* Effect.forEach(rows, decodeMediaReplacement);
|
|
8549
|
-
}).pipe(mapPersistenceFailure$
|
|
7127
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pendingReplacements")));
|
|
8550
7128
|
const inspectReplacement = Effect.fn("MediaStore.Drizzle.inspectReplacement")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8551
7129
|
const asset = yield* lockedRowById(transaction, replacement.mediaId);
|
|
8552
7130
|
const pending = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
@@ -8555,17 +7133,17 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8555
7133
|
return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
8556
7134
|
}
|
|
8557
7135
|
return asset && isCompletedReplacement(asset, replacement) ? "completed" : "absent";
|
|
8558
|
-
})).pipe(mapStoreFailure$
|
|
7136
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.inspectReplacement")));
|
|
8559
7137
|
const inventory = Effect.gen(function* () {
|
|
8560
7138
|
const rows = yield* database.select().from(mediaAssets).where(eq(mediaAssets.status, "ready")).orderBy(asc(mediaAssets.objectKey));
|
|
8561
7139
|
return yield* Effect.forEach(rows, decodeMediaRecord);
|
|
8562
|
-
}).pipe(mapPersistenceFailure$
|
|
7140
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.inventory"), Effect.withSpan("MediaStore.Drizzle.inventory"));
|
|
8563
7141
|
const knownObjectKeys = Effect.gen(function* () {
|
|
8564
7142
|
const assetRows = yield* database.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).orderBy(asc(mediaAssets.objectKey));
|
|
8565
7143
|
const replacementRows = yield* database.select({ objectKey: mediaReplacements.stagingObjectKey }).from(mediaReplacements).orderBy(asc(mediaReplacements.stagingObjectKey));
|
|
8566
7144
|
const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))([...assetRows, ...replacementRows].map((row) => row.objectKey));
|
|
8567
7145
|
return new Set(objectKeys);
|
|
8568
|
-
}).pipe(mapPersistenceFailure$
|
|
7146
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.knownObjectKeys"), Effect.withSpan("MediaStore.Drizzle.knownObjectKeys"));
|
|
8569
7147
|
const search = Effect.fn("MediaStore.Drizzle.search")((query) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8570
7148
|
const normalizedSearch = query.search?.trim();
|
|
8571
7149
|
const pattern = normalizedSearch ? `%${escapeLike(normalizedSearch)}%` : void 0;
|
|
@@ -8574,14 +7152,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8574
7152
|
const rows = yield* transaction.select().from(mediaAssets).where(and(filters, cursorFilter)).orderBy(desc(mediaAssets.createdAt), desc(mediaAssets.id)).limit(query.limit + 1);
|
|
8575
7153
|
const folderRows = yield* transaction.selectDistinct({ folder: mediaAssets.folder }).from(mediaAssets).where(and(eq(mediaAssets.status, "ready"), ne(mediaAssets.folder, ""))).orderBy(asc(mediaAssets.folder));
|
|
8576
7154
|
const total = (yield* transaction.select({ value: count() }).from(mediaAssets).where(filters))[0]?.value;
|
|
8577
|
-
if (total === void 0) return yield* persistenceFailure$
|
|
7155
|
+
if (total === void 0) return yield* persistenceFailure$2("MediaStore.Drizzle.search", /* @__PURE__ */ new Error("Media persistence did not return a search total."));
|
|
8578
7156
|
const decoded = yield* Effect.forEach(rows, decodeMediaRecord);
|
|
8579
7157
|
const folders = yield* Schema.decodeEffect(Schema.Array(MediaFolder))(folderRows.map((row) => row.folder)).pipe(Effect.mapError((cause) => mediaPersistenceError("MediaStore.Drizzle.search", cause)));
|
|
8580
7158
|
return mediaPage(decoded, query.limit, folders, total);
|
|
8581
7159
|
}), {
|
|
8582
7160
|
accessMode: "read only",
|
|
8583
7161
|
isolationLevel: "repeatable read"
|
|
8584
|
-
}).pipe(mapPersistenceFailure$
|
|
7162
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.search")));
|
|
8585
7163
|
return MediaStore.of({
|
|
8586
7164
|
abandonUpload,
|
|
8587
7165
|
activate,
|
|
@@ -8666,45 +7244,230 @@ const replacementByMediaId = Effect.fn("MediaStore.Drizzle.replacementByMediaId"
|
|
|
8666
7244
|
return rows[0] ? yield* decodeMediaReplacement(rows[0]) : void 0;
|
|
8667
7245
|
});
|
|
8668
7246
|
function transitionFailure(operation, id, actualStatus) {
|
|
8669
|
-
return persistenceFailure$
|
|
7247
|
+
return persistenceFailure$2(operation, /* @__PURE__ */ new Error(actualStatus === void 0 ? `Media record "${id}" does not exist.` : `Media record "${id}" cannot transition from "${actualStatus}".`));
|
|
8670
7248
|
}
|
|
8671
|
-
function mapPersistenceFailure$
|
|
7249
|
+
function mapPersistenceFailure$2(operation) {
|
|
8672
7250
|
return (effect) => effect.pipe(Effect.mapError((cause) => isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
|
|
8673
7251
|
}
|
|
8674
|
-
function mapStoreFailure$
|
|
7252
|
+
function mapStoreFailure$2(operation) {
|
|
8675
7253
|
return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
|
|
8676
7254
|
}
|
|
8677
7255
|
function mapBeginDeleteFailure(operation) {
|
|
8678
7256
|
return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaNotFound(cause) || isMediaPersistenceError(cause) || isMediaReferenced(cause) ? cause : mediaPersistenceError(operation, cause)));
|
|
8679
7257
|
}
|
|
8680
|
-
function persistenceFailure$
|
|
7258
|
+
function persistenceFailure$2(operation, cause) {
|
|
8681
7259
|
return Effect.fail(mediaPersistenceError(operation, cause));
|
|
8682
7260
|
}
|
|
8683
7261
|
|
|
8684
7262
|
//#endregion
|
|
8685
|
-
//#region ../sql-pg/src/internal/
|
|
8686
|
-
const
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
7263
|
+
//#region ../sql-pg/src/internal/seo/seo-store.ts
|
|
7264
|
+
const SeoStoreDomainError = Schema.Union([
|
|
7265
|
+
SeoMediaUnavailable,
|
|
7266
|
+
SeoPersistenceError,
|
|
7267
|
+
SeoVersionConflict
|
|
7268
|
+
]);
|
|
7269
|
+
const isSeoStoreDomainError = Schema.is(SeoStoreDomainError);
|
|
7270
|
+
const isSeoPersistenceError = Schema.is(SeoPersistenceError);
|
|
7271
|
+
const systemActivity = {
|
|
7272
|
+
actor: {
|
|
7273
|
+
email: "system@cms.invalid",
|
|
7274
|
+
id: "system",
|
|
7275
|
+
name: "CMS System",
|
|
7276
|
+
roles: ["system"]
|
|
7277
|
+
},
|
|
7278
|
+
requestId: RequestId.make("00000000-0000-4000-8000-000000000000")
|
|
7279
|
+
};
|
|
7280
|
+
const seoStoreLayer = Layer.effect(SeoStore, Effect.gen(function* () {
|
|
7281
|
+
const database = yield* PgDrizzle.makeWithDefaults();
|
|
7282
|
+
const initialize = Effect.fn("SeoStore.Drizzle.initialize")((defaults) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7283
|
+
yield* transaction.execute(sql`SELECT pg_advisory_xact_lock(hashtext('cms:seo:singleton:v1'))`);
|
|
7284
|
+
const current = yield* readSnapshot(transaction);
|
|
7285
|
+
if (current) return current;
|
|
7286
|
+
yield* ensureMediaAvailable(transaction, defaults).pipe(Effect.mapError((cause) => new SeoPersistenceError({
|
|
7287
|
+
cause,
|
|
7288
|
+
operation: "SeoStore.Drizzle.initializeDefaults"
|
|
7289
|
+
})));
|
|
7290
|
+
const revision = yield* insertRevision(transaction, defaults, systemActivity);
|
|
7291
|
+
yield* transaction.insert(seoState).values({
|
|
7292
|
+
publishedRevisionId: revision.revisionId,
|
|
7293
|
+
workingRevisionId: revision.revisionId
|
|
8703
7294
|
});
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
7295
|
+
yield* replaceMediaReferences(transaction, "working", defaults);
|
|
7296
|
+
yield* replaceMediaReferences(transaction, "published", defaults);
|
|
7297
|
+
return {
|
|
7298
|
+
published: revision,
|
|
7299
|
+
working: revision
|
|
7300
|
+
};
|
|
7301
|
+
})).pipe(mapStoreFailure$1("SeoStore.Drizzle.initialize")));
|
|
7302
|
+
const load = database.transaction((transaction) => readSnapshot(transaction), {
|
|
7303
|
+
accessMode: "read only",
|
|
7304
|
+
isolationLevel: "repeatable read"
|
|
7305
|
+
}).pipe(mapPersistenceFailure$1("SeoStore.Drizzle.load"));
|
|
7306
|
+
const save = Effect.fn("SeoStore.Drizzle.save")((input) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7307
|
+
const current = yield* lockedSnapshot(transaction);
|
|
7308
|
+
yield* requireVersion(current, input.expectedVersion);
|
|
7309
|
+
yield* ensureMediaAvailable(transaction, input.settings);
|
|
7310
|
+
const revision = yield* insertRevision(transaction, input.settings, input.activity);
|
|
7311
|
+
yield* transaction.update(seoState).set({ workingRevisionId: revision.revisionId }).where(eq(seoState.singleton, true));
|
|
7312
|
+
yield* replaceMediaReferences(transaction, "working", input.settings);
|
|
7313
|
+
yield* recordActivity(transaction, "seo.saved", input.activity, {
|
|
7314
|
+
previousVersion: current.working.version,
|
|
7315
|
+
version: revision.version
|
|
7316
|
+
}, ActivityId.make(revision.revisionId));
|
|
7317
|
+
return {
|
|
7318
|
+
...current,
|
|
7319
|
+
working: revision
|
|
7320
|
+
};
|
|
7321
|
+
})).pipe(mapStoreFailure$1("SeoStore.Drizzle.save")));
|
|
7322
|
+
const publish = Effect.fn("SeoStore.Drizzle.publish")((input) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7323
|
+
const current = yield* lockedSnapshot(transaction);
|
|
7324
|
+
yield* requireVersion(current, input.expectedVersion);
|
|
7325
|
+
yield* ensureMediaAvailable(transaction, current.working.settings);
|
|
7326
|
+
yield* transaction.update(seoState).set({ publishedRevisionId: current.working.revisionId }).where(eq(seoState.singleton, true));
|
|
7327
|
+
yield* replaceMediaReferences(transaction, "published", current.working.settings);
|
|
7328
|
+
yield* recordActivity(transaction, "seo.published", input.activity, { version: current.working.version });
|
|
7329
|
+
return {
|
|
7330
|
+
...current,
|
|
7331
|
+
published: current.working
|
|
7332
|
+
};
|
|
7333
|
+
})).pipe(mapStoreFailure$1("SeoStore.Drizzle.publish")));
|
|
7334
|
+
return SeoStore.of({
|
|
7335
|
+
initialize,
|
|
7336
|
+
load,
|
|
7337
|
+
publish,
|
|
7338
|
+
save
|
|
7339
|
+
});
|
|
8707
7340
|
}));
|
|
7341
|
+
const readSnapshot = Effect.fn("SeoStore.Drizzle.readSnapshot")(function* (database) {
|
|
7342
|
+
const state = (yield* database.select().from(seoState).limit(1))[0];
|
|
7343
|
+
if (!state) return void 0;
|
|
7344
|
+
const revisionRows = yield* database.select().from(seoRevisions).where(inArray(seoRevisions.id, [state.workingRevisionId, state.publishedRevisionId]));
|
|
7345
|
+
const revisions = yield* Effect.forEach(revisionRows, decodeRevision);
|
|
7346
|
+
const working = revisions.find((revision) => revision.revisionId === state.workingRevisionId);
|
|
7347
|
+
const published = revisions.find((revision) => revision.revisionId === state.publishedRevisionId);
|
|
7348
|
+
if (!working || !published) return yield* persistenceFailure$1("SeoStore.Drizzle.readSnapshot", /* @__PURE__ */ new Error("SEO state points to a missing revision."));
|
|
7349
|
+
return {
|
|
7350
|
+
published,
|
|
7351
|
+
working
|
|
7352
|
+
};
|
|
7353
|
+
});
|
|
7354
|
+
const lockedSnapshot = Effect.fn("SeoStore.Drizzle.lockSnapshot")(function* (transaction) {
|
|
7355
|
+
if (!(yield* transaction.select().from(seoState).where(eq(seoState.singleton, true)).for("update").limit(1))[0]) return yield* persistenceFailure$1("SeoStore.Drizzle.lockSnapshot", /* @__PURE__ */ new Error("SEO state is not initialized."));
|
|
7356
|
+
return (yield* readSnapshot(transaction)) ?? (yield* persistenceFailure$1("SeoStore.Drizzle.lockSnapshot", /* @__PURE__ */ new Error("SEO state disappeared.")));
|
|
7357
|
+
});
|
|
7358
|
+
const insertRevision = Effect.fn("SeoStore.Drizzle.insertRevision")(function* (transaction, settings, activity) {
|
|
7359
|
+
const row = (yield* transaction.insert(seoRevisions).values({
|
|
7360
|
+
actorEmail: activity.actor.email,
|
|
7361
|
+
actorId: activity.actor.id,
|
|
7362
|
+
actorName: activity.actor.name,
|
|
7363
|
+
actorRoles: activity.actor.roles,
|
|
7364
|
+
reason: activity.reason,
|
|
7365
|
+
requestId: activity.requestId,
|
|
7366
|
+
settings
|
|
7367
|
+
}).returning())[0];
|
|
7368
|
+
return row ? yield* decodeRevision(row) : yield* persistenceFailure$1("SeoStore.Drizzle.insertRevision", /* @__PURE__ */ new Error("No revision row returned."));
|
|
7369
|
+
});
|
|
7370
|
+
const decodeRevision = Effect.fn("SeoStore.Drizzle.decodeRevision")(function* (row) {
|
|
7371
|
+
return yield* Schema.decodeEffect(SeoRevision)({
|
|
7372
|
+
editedAt: row.createdAt,
|
|
7373
|
+
editedBy: {
|
|
7374
|
+
email: row.actorEmail,
|
|
7375
|
+
id: row.actorId,
|
|
7376
|
+
name: row.actorName,
|
|
7377
|
+
roles: row.actorRoles
|
|
7378
|
+
},
|
|
7379
|
+
revisionId: row.id,
|
|
7380
|
+
settings: row.settings,
|
|
7381
|
+
version: row.id
|
|
7382
|
+
}).pipe(Effect.mapError((cause) => new SeoPersistenceError({
|
|
7383
|
+
cause,
|
|
7384
|
+
operation: "SeoStore.Drizzle.decodeRevision"
|
|
7385
|
+
})));
|
|
7386
|
+
});
|
|
7387
|
+
function requireVersion(snapshot, expectedVersion) {
|
|
7388
|
+
return snapshot.working.version === expectedVersion ? Effect.void : Effect.fail(new SeoVersionConflict({
|
|
7389
|
+
actualVersion: snapshot.working.version,
|
|
7390
|
+
expectedVersion
|
|
7391
|
+
}));
|
|
7392
|
+
}
|
|
7393
|
+
const ensureMediaAvailable = Effect.fn("SeoStore.Drizzle.ensureMediaAvailable")(function* (transaction, settings) {
|
|
7394
|
+
const mediaIds = settingsMediaIds(settings);
|
|
7395
|
+
if (mediaIds.length === 0) return;
|
|
7396
|
+
const initialRows = yield* transaction.select({
|
|
7397
|
+
id: mediaAssets.id,
|
|
7398
|
+
objectKey: mediaAssets.objectKey
|
|
7399
|
+
}).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
|
|
7400
|
+
const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(initialRows.map((row) => row.objectKey));
|
|
7401
|
+
yield* lockMediaReferenceKeys(transaction, objectKeys);
|
|
7402
|
+
const readyRows = yield* transaction.select({
|
|
7403
|
+
id: mediaAssets.id,
|
|
7404
|
+
objectKey: mediaAssets.objectKey
|
|
7405
|
+
}).from(mediaAssets).where(and(inArray(mediaAssets.id, mediaIds), eq(mediaAssets.status, "ready")));
|
|
7406
|
+
const unavailable = settingsMediaImages(settings).find((image) => {
|
|
7407
|
+
const asset = readyRows.find((row) => row.id === image.mediaId);
|
|
7408
|
+
return !asset || mediaObjectKey(image.url) !== asset.objectKey;
|
|
7409
|
+
});
|
|
7410
|
+
if (unavailable) return yield* new SeoMediaUnavailable({ mediaId: unavailable.mediaId });
|
|
7411
|
+
});
|
|
7412
|
+
const replaceMediaReferences = Effect.fn("SeoStore.Drizzle.replaceMediaReferences")(function* (transaction, scope, settings) {
|
|
7413
|
+
yield* transaction.delete(seoMediaReferences).where(eq(seoMediaReferences.scope, scope));
|
|
7414
|
+
const mediaIds = settingsMediaIds(settings);
|
|
7415
|
+
if (mediaIds.length > 0) yield* transaction.insert(seoMediaReferences).values(mediaIds.map((mediaId) => ({
|
|
7416
|
+
mediaId,
|
|
7417
|
+
scope
|
|
7418
|
+
})));
|
|
7419
|
+
});
|
|
7420
|
+
function settingsMediaIds(settings) {
|
|
7421
|
+
return [...new Set(settingsMediaImages(settings).map((image) => image.mediaId))];
|
|
7422
|
+
}
|
|
7423
|
+
function settingsMediaImages(settings) {
|
|
7424
|
+
const images = [settings.socialImage];
|
|
7425
|
+
const identity = settings.identity;
|
|
7426
|
+
if (identity?.type === "Person") images.push(identity.image);
|
|
7427
|
+
if (identity?.type === "Organization") images.push(identity.logo);
|
|
7428
|
+
if (identity?.type === "LocalBusiness") images.push(identity.logo, identity.image);
|
|
7429
|
+
return images.filter((image) => image !== void 0);
|
|
7430
|
+
}
|
|
7431
|
+
function mediaObjectKey(url) {
|
|
7432
|
+
try {
|
|
7433
|
+
const encodedKey = new URL(url).pathname.match(/(?:^|\/)(media\/[^/]+)$/u)?.[1];
|
|
7434
|
+
return encodedKey ? encodedKey.split("/").map(decodeURIComponent).join("/") : void 0;
|
|
7435
|
+
} catch {
|
|
7436
|
+
return;
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
const recordActivity = Effect.fn("SeoStore.Drizzle.recordActivity")(function* (transaction, action, context, details, id) {
|
|
7440
|
+
yield* transaction.insert(activityEntries).values({
|
|
7441
|
+
action,
|
|
7442
|
+
actorEmail: context.actor.email,
|
|
7443
|
+
actorId: context.actor.id,
|
|
7444
|
+
actorName: context.actor.name,
|
|
7445
|
+
actorRoles: context.actor.roles,
|
|
7446
|
+
details,
|
|
7447
|
+
id,
|
|
7448
|
+
reason: context.reason,
|
|
7449
|
+
requestId: context.requestId,
|
|
7450
|
+
targetKind: "seo"
|
|
7451
|
+
});
|
|
7452
|
+
});
|
|
7453
|
+
function mapStoreFailure$1(operation) {
|
|
7454
|
+
return (effect) => effect.pipe(Effect.mapError((cause) => isSeoStoreDomainError(cause) ? cause : new SeoPersistenceError({
|
|
7455
|
+
cause,
|
|
7456
|
+
operation
|
|
7457
|
+
})));
|
|
7458
|
+
}
|
|
7459
|
+
function mapPersistenceFailure$1(operation) {
|
|
7460
|
+
return (effect) => effect.pipe(Effect.mapError((cause) => isSeoPersistenceError(cause) ? cause : new SeoPersistenceError({
|
|
7461
|
+
cause,
|
|
7462
|
+
operation
|
|
7463
|
+
})));
|
|
7464
|
+
}
|
|
7465
|
+
function persistenceFailure$1(operation, cause) {
|
|
7466
|
+
return Effect.fail(new SeoPersistenceError({
|
|
7467
|
+
cause,
|
|
7468
|
+
operation
|
|
7469
|
+
}));
|
|
7470
|
+
}
|
|
8708
7471
|
|
|
8709
7472
|
//#endregion
|
|
8710
7473
|
//#region ../sql-pg/src/internal/user-administration/user-command-fence.ts
|
|
@@ -8722,7 +7485,7 @@ const postgresUserCommandFenceLayer = Layer.effect(UserCommandFence, PostgresAdv
|
|
|
8722
7485
|
const WORKING_DOCUMENT_CHANGES_CHANNEL = "cms_working_documents";
|
|
8723
7486
|
const WorkingDocumentChangePayload = Schema.fromJsonString(WorkingDocumentChange);
|
|
8724
7487
|
function workingDocumentChangePayload(change) {
|
|
8725
|
-
return Schema.
|
|
7488
|
+
return Result.getOrThrow(Schema.encodeResult(WorkingDocumentChangePayload)(change));
|
|
8726
7489
|
}
|
|
8727
7490
|
const decodeWorkingDocumentChangePayload = Schema.decodeUnknownEffect(WorkingDocumentChangePayload);
|
|
8728
7491
|
|
|
@@ -8757,12 +7520,13 @@ const makePostgresChanges = Effect.fn("WorkingDocumentChanges.makePostgres")(fun
|
|
|
8757
7520
|
const postgres = Context.get(context, PgClient.PgClient);
|
|
8758
7521
|
const heartbeats = /* @__PURE__ */ new Map();
|
|
8759
7522
|
let heartbeat = 0;
|
|
8760
|
-
const
|
|
8761
|
-
|
|
7523
|
+
const notifications = yield* postgres.listen(WORKING_DOCUMENT_CHANGES_CHANNEL).pipe(Effect.mapError((cause) => changesError("listen", cause)));
|
|
7524
|
+
const listener = yield* Stream.fromQueue(notifications).pipe(Stream.runForEach((payload) => {
|
|
7525
|
+
const waiting = heartbeats.get(payload.payload);
|
|
8762
7526
|
if (waiting !== void 0) return Deferred.succeed(waiting, void 0).pipe(Effect.asVoid);
|
|
8763
|
-
if (payload.startsWith("__cms_probe__:")) return Effect.void;
|
|
8764
|
-
return decodeWorkingDocumentChangePayload(payload).pipe(Effect.flatMap((change) => PubSub.publish(changes, change)), Effect.asVoid, Effect.catchCause((cause) => Effect.logWarning("Ignored an invalid Working Document change.", Cause.pretty(cause))));
|
|
8765
|
-
}), Effect.
|
|
7527
|
+
if (payload.payload.startsWith("__cms_probe__:")) return Effect.void;
|
|
7528
|
+
return decodeWorkingDocumentChangePayload(payload.payload).pipe(Effect.flatMap((change) => PubSub.publish(changes, change)), Effect.asVoid, Effect.catchCause((cause) => Effect.logWarning("Ignored an invalid Working Document change.", Cause.pretty(cause))));
|
|
7529
|
+
}), Effect.catchCause((cause) => Effect.fail(changesError("listen", cause)))).pipe(Effect.forkScoped({ startImmediately: true }));
|
|
8766
7530
|
const pulse = Effect.gen(function* () {
|
|
8767
7531
|
heartbeat += 1;
|
|
8768
7532
|
const payload = `__cms_probe__:${session}:${heartbeat}`;
|
|
@@ -9320,16 +8084,13 @@ function postgresLayer(options) {
|
|
|
9320
8084
|
});
|
|
9321
8085
|
const migrations = cmsDatabaseMigrationLayer.pipe(Layer.provide(database));
|
|
9322
8086
|
const locks = postgresAdvisoryLockLayer(options);
|
|
9323
|
-
|
|
9324
|
-
const readiness = postgresReadinessLayer.pipe(Layer.provide(stores), Layer.provide(database));
|
|
9325
|
-
return Layer.merge(stores, readiness);
|
|
8087
|
+
return Layer.mergeAll(postgresActivityStoreLayer, changeJournalLayer, documentStoreLayer, mediaStoreLayer, seoStoreLayer, initializationStoreLayer.pipe(Layer.provide(locks)), postgresUserCommandFenceLayer.pipe(Layer.provide(locks)), postgresWorkingDocumentChangesLayer(options), workingDocumentStoreLayer).pipe(Layer.provide(migrations), Layer.provide(database));
|
|
9326
8088
|
}
|
|
9327
8089
|
|
|
9328
8090
|
//#endregion
|
|
9329
8091
|
//#region src/runtime/installation.ts
|
|
9330
|
-
/** Composes the private providers selected by the Nuxt product adapter. */
|
|
9331
8092
|
const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options) {
|
|
9332
|
-
const crypto = NodeCrypto
|
|
8093
|
+
const crypto = NodeCrypto.layer;
|
|
9333
8094
|
const postgres = postgresLayer({ url: Redacted.make(options.databaseUrl) });
|
|
9334
8095
|
const identity = Layer.unwrap(Effect.acquireRelease(Effect.sync(() => new Pool({ connectionString: options.databaseUrl })), (pool) => Effect.promise(() => pool.end()).pipe(Effect.asVoid)).pipe(Effect.map((pool) => betterAuthLayer({
|
|
9335
8096
|
baseURL: options.baseUrl,
|
|
@@ -9339,6 +8100,7 @@ const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options)
|
|
|
9339
8100
|
const core = Layer.mergeAll(crypto, postgres, identity);
|
|
9340
8101
|
return yield* installCms$1({
|
|
9341
8102
|
baseUrl: options.baseUrl,
|
|
8103
|
+
formHandlers: options.formHandlers,
|
|
9342
8104
|
authoring: options.authoring,
|
|
9343
8105
|
bootstrap: options.bootstrap,
|
|
9344
8106
|
cleanup: options.cleanup.enabled ? {
|
|
@@ -9353,7 +8115,6 @@ const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options)
|
|
|
9353
8115
|
} : void 0
|
|
9354
8116
|
},
|
|
9355
8117
|
documentModel: options.documentModel,
|
|
9356
|
-
initialSources: options.initialSources,
|
|
9357
8118
|
initializationToken: Redacted.make(options.initializationToken),
|
|
9358
8119
|
studioSecret: Redacted.make(`cms-collaboration:${options.secret}`)
|
|
9359
8120
|
});
|
|
@@ -9377,4 +8138,4 @@ function toS3MediaObjectStoreOptions(options) {
|
|
|
9377
8138
|
|
|
9378
8139
|
//#endregion
|
|
9379
8140
|
export { toS3MediaObjectStoreOptions as n, installCms as t };
|
|
9380
|
-
//# sourceMappingURL=installation-
|
|
8141
|
+
//# sourceMappingURL=installation-CNfIriVm.js.map
|