@phtngyn/cms 0.1.0-beta.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -6
- package/package.json +31 -16
- package/src/Authoring.d.ts +13 -1
- package/src/Configuration-Br7tE9Qt.d.ts +68 -0
- package/src/{AuthoringSchema-BQ5kbhVB.js → Configuration-CA_jvQKG.js} +599 -492
- package/src/Configuration-CA_jvQKG.js.map +1 -0
- package/src/Configuration.d.ts +2 -44
- package/src/Configuration.js +0 -1
- package/src/Configuration.js.map +1 -1
- package/src/{DocumentEngine-Da0kWotL.js → DocumentEngine-CgiBQU4g.js} +110 -84
- package/src/DocumentEngine-CgiBQU4g.js.map +1 -0
- package/src/DocumentModel-BB9igGC7.d.ts +34 -0
- package/src/DocumentModel.d.ts +2 -30
- 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-DxbkU3_5.js +367 -0
- package/src/Forms-DxbkU3_5.js.map +1 -0
- package/src/Forms.d.ts +35 -0
- package/src/Forms.js +13 -0
- package/src/Forms.js.map +1 -0
- package/src/HttpPaths-c9nLZ9H2.js +21 -0
- package/src/HttpPaths-c9nLZ9H2.js.map +1 -0
- package/src/I18n-DZK4CkJF.d.ts +21 -0
- package/src/I18n.d.ts +2 -0
- package/src/I18n.js +1 -0
- package/src/Module.js +78 -81
- package/src/Module.js.map +1 -1
- package/src/S3MediaObjects-JKMAWyVB.js +2664 -0
- package/src/S3MediaObjects-JKMAWyVB.js.map +1 -0
- package/src/Seed.d.ts +6 -0
- package/src/Seed.js +34 -0
- package/src/Seed.js.map +1 -0
- package/src/Seo-402YumpT.d.ts +198 -0
- package/src/{connection-ZGtL54qY.js → connection-B-iz0Y9Y.js} +5 -7
- package/src/connection-B-iz0Y9Y.js.map +1 -0
- package/src/{installation-BNKaVjni.js → installation-DgF2M1gL.js} +1162 -2147
- package/src/installation-DgF2M1gL.js.map +1 -0
- package/src/internal/authoring-catalog.js +153 -0
- package/src/internal/authoring-catalog.js.map +1 -0
- package/src/internal/authoring-compiler.js +2 -2
- package/src/internal/authoring-compiler.js.map +1 -1
- package/src/internal/authoring-metadata.js +68 -49
- 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 +25 -13
- package/src/internal/configuration.js.map +1 -1
- package/src/internal/document-model.js +66 -20
- 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 +66 -0
- package/src/internal/studio-app.js.map +1 -0
- package/src/location-CFKTeK1K.js +35 -0
- package/src/location-CFKTeK1K.js.map +1 -0
- package/src/{metadata-Czvi3E4K.js → metadata-BEZPWh-3.js} +3 -72
- package/src/metadata-BEZPWh-3.js.map +1 -0
- package/src/{models-DS5ClMsH.js → models-BWm1pTAf.js} +9 -31
- package/src/models-BWm1pTAf.js.map +1 -0
- package/src/models-DwYioyNV.js +726 -0
- package/src/models-DwYioyNV.js.map +1 -0
- package/src/runtime/blocks.js +30 -0
- package/src/runtime/blocks.js.map +1 -0
- package/src/runtime/collaboration.js +9 -4
- package/src/runtime/collaboration.js.map +1 -1
- package/src/runtime/delivery/DocumentRenderer.js +10 -4
- package/src/runtime/delivery/DocumentRenderer.js.map +1 -1
- package/src/runtime/delivery/blocks.js +8 -0
- package/src/runtime/delivery/blocks.js.map +1 -0
- package/src/runtime/delivery/context.js +12 -0
- package/src/runtime/delivery/context.js.map +1 -0
- package/src/runtime/delivery/document-page.js +14 -12
- package/src/runtime/delivery/document-page.js.map +1 -1
- package/src/runtime/delivery/document.js +21 -3
- package/src/runtime/delivery/document.js.map +1 -1
- package/src/runtime/delivery/error.js +11 -0
- package/src/runtime/delivery/error.js.map +1 -0
- package/src/runtime/delivery/plugin.js +41 -0
- package/src/runtime/delivery/plugin.js.map +1 -0
- package/src/runtime/delivery/types.d.ts +5 -0
- package/src/runtime/delivery/useCmsDocumentSeo.d.ts +56 -0
- package/src/runtime/delivery/useCmsDocumentSeo.js +99 -0
- package/src/runtime/delivery/useCmsDocumentSeo.js.map +1 -0
- package/src/runtime/delivery/useDocument.js +5 -2
- package/src/runtime/delivery/useDocument.js.map +1 -1
- package/src/runtime/delivery/useDocumentQuery.js +5 -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 +149 -0
- package/src/runtime/forms/useCmsForm.js.map +1 -0
- package/src/runtime/i18n/translator.js +22 -0
- package/src/runtime/i18n/translator.js.map +1 -0
- package/src/runtime/i18n/useCmsI18n.d.ts +9 -0
- package/src/runtime/i18n/useCmsI18n.js +29 -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 +37 -11
- package/src/runtime/preview/controller.js.map +1 -1
- package/src/runtime/preview/overlay.js +65 -0
- package/src/runtime/preview/overlay.js.map +1 -0
- package/src/runtime/preview/plugin.client.js +59 -61
- package/src/runtime/preview/plugin.client.js.map +1 -1
- package/src/runtime/preview/preview.css +72 -4
- 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 +5 -1
- package/src/runtime/studio-app.js.map +1 -1
- package/src/runtime/studio-entry.js +36 -0
- package/src/runtime/studio-entry.js.map +1 -0
- package/src/runtime/studio-navigation.js +11 -0
- package/src/runtime/studio-navigation.js.map +1 -0
- package/src/scalars-B2lgGUnj.js +26 -0
- package/src/scalars-B2lgGUnj.js.map +1 -0
- package/src/{store-Nl4uxEXx.js → store-DjJs3I7k.js} +12 -7
- package/src/store-DjJs3I7k.js.map +1 -0
- package/studio/CreateDocumentDialog-CTeIFlPb.js +256 -0
- package/studio/collaboration-DcK2F6v9.js +4566 -0
- package/studio/documentPlanning-Bfv-d7OU.js +29820 -0
- package/studio/dom-to-image-more.min-D6jGGdrW.js +877 -0
- package/studio/editor-Bzlh3zXt.js +30827 -0
- package/studio/host.css +3 -0
- package/studio/host.js +46614 -0
- package/studio/rolldown-runtime-B0aSnxlc.js +20 -0
- package/dist/studio/assets/ActivityEntryRow-D--fRvWI.js +0 -1
- package/dist/studio/assets/ActivityView-D-XBBUHb.js +0 -1
- package/dist/studio/assets/CreateDocumentDialog-CH8UH1QM.js +0 -1
- package/dist/studio/assets/DocumentsView-CxRpmAxt.js +0 -34
- package/dist/studio/assets/DropdownMenu-BSb1l7fr.js +0 -1
- package/dist/studio/assets/Input-DVao38h7.js +0 -3
- package/dist/studio/assets/MediaView-BHrPIjxB.js +0 -1
- package/dist/studio/assets/Modal-VNrXGa5C.js +0 -1
- package/dist/studio/assets/OverviewView-DfP6-mLZ.js +0 -1
- package/dist/studio/assets/Select-BJZIs9sY.js +0 -1
- package/dist/studio/assets/SettingsView-CerHe-ua.js +0 -1
- package/dist/studio/assets/Skeleton-CWJNi3nS.js +0 -1
- package/dist/studio/assets/StudioPageHeader-DP9HDAF2.js +0 -1
- package/dist/studio/assets/UsersView-cP3REkH2.js +0 -1
- package/dist/studio/assets/collaboration-BLnCzfvV.js +0 -4
- package/dist/studio/assets/context-dY4fNbn3.js +0 -1
- package/dist/studio/assets/editor-CE07wW83.js +0 -137
- package/dist/studio/assets/gateway-BAyQgCse.js +0 -10
- package/dist/studio/assets/index-CjoaPLhZ.js +0 -28
- package/dist/studio/assets/index-s2rPqCmP.css +0 -2
- package/dist/studio/assets/render-BkxbK2St.js +0 -83
- package/dist/studio/assets/rolldown-runtime-DK3Fl9T5.js +0 -1
- package/dist/studio/assets/useMediaLibrary-D7qR7aBf.js +0 -1
- package/dist/studio/index.html +0 -76
- package/src/AuthoringSchema-BQ5kbhVB.js.map +0 -1
- package/src/Configuration-BGYFD2N1.js +0 -150
- package/src/Configuration-BGYFD2N1.js.map +0 -1
- package/src/DocumentEngine-Da0kWotL.js.map +0 -1
- package/src/HttpPaths-86qLGB90.js +0 -15
- package/src/HttpPaths-86qLGB90.js.map +0 -1
- package/src/StudioHttp-BTLLooh0.js +0 -916
- package/src/StudioHttp-BTLLooh0.js.map +0 -1
- package/src/connection-ZGtL54qY.js.map +0 -1
- package/src/installation-BNKaVjni.js.map +0 -1
- package/src/internal/component-metadata.js +0 -37
- package/src/internal/component-metadata.js.map +0 -1
- package/src/metadata-Czvi3E4K.js.map +0 -1
- package/src/models-Citx8jBi.js +0 -306
- package/src/models-Citx8jBi.js.map +0 -1
- package/src/models-DS5ClMsH.js.map +0 -1
- package/src/store-Nl4uxEXx.js.map +0 -1
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
4
|
-
import {
|
|
5
|
-
import { c as
|
|
6
|
-
import { a as
|
|
7
|
-
import { a as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { HttpApiBuilder, HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
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-DxbkU3_5.js";
|
|
4
|
+
import { A as RemoveDocumentInput, C as MarkdownNode, Ct as MediaTooLarge, G as SeoRevision, H as SeoConfirmationRequired, M as UndeleteDocumentInput, N as UnpublishDocumentInput, P as UpdateDocumentInput, Q as AuthenticationRequired, S as DuplicateDocumentInput, St as MediaStatus, T as PublishDocumentInput, U as SeoMediaUnavailable, Y as SeoVersionConflict, Z as AccessDenied, _ as DocumentSource, _t as MediaObjectKey, a as DOCUMENT_REVISION_ACTIONS, at as ImageMimeType, bt as MediaReference, c as DocumentImportInput, ct as MediaCleanupReport, dt as MediaFolder, f as DocumentMutationResult, ft as MediaId, g as DocumentRevisionAction, gt as MediaNotFound, ht as MediaName, it as StudioRequestContext, j as RestoreDocumentInput, kt as RequestId, lt as MediaConflict, mt as MediaInvalid, n as CmsMarkdownDocument, nt as StudioActor, o as DeletedDocument, ot as MediaAsset, pt as MediaIntegrityReport, rt as StudioEntryDenied, st as MediaByteSize, tt as Role, ut as MediaCursor, vt as MediaPage, w as MoveDocumentInput, wt as MediaUpload, xt as MediaReferenced } from "./models-DwYioyNV.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-JKMAWyVB.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-CA_jvQKG.js";
|
|
9
|
+
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";
|
|
11
10
|
import { Extension, Node, getSchema, mergeAttributes } from "@tiptap/core";
|
|
12
11
|
import { prosemirrorJSONToYDoc, ySyncPluginKey, yXmlFragmentToProseMirrorRootNode } from "@tiptap/y-tiptap";
|
|
12
|
+
import { FindMyWay, HttpEffect, HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse, Multipart } from "effect/unstable/http";
|
|
13
|
+
import { HttpApiBuilder, HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
13
14
|
import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto";
|
|
15
|
+
import { createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
|
|
14
16
|
import { betterAuth } from "better-auth";
|
|
15
17
|
import { createAccessControl } from "better-auth/plugins/access";
|
|
16
18
|
import { admin } from "better-auth/plugins/admin";
|
|
17
19
|
import { defaultStatements } from "better-auth/plugins/admin/access";
|
|
18
|
-
import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
|
|
19
20
|
import { Hocuspocus, IncomingMessage, MessageType, shouldSkipStoreHooks } from "@hocuspocus/server";
|
|
20
|
-
import { renderMarkdown } from "comark/render";
|
|
21
|
-
import { diffArrays } from "diff";
|
|
22
21
|
import StarterKit from "@tiptap/starter-kit";
|
|
23
22
|
import * as Y from "yjs";
|
|
24
23
|
import { Doc, applyUpdate, encodeStateAsUpdate, encodeStateVector } from "yjs";
|
|
25
|
-
import {
|
|
26
|
-
import { Readable } from "node:stream";
|
|
24
|
+
import { RateLimiter } from "effect/unstable/persistence";
|
|
27
25
|
import { PgClient } from "@effect/sql-pg";
|
|
28
26
|
import { isDeepStrictEqual } from "node:util";
|
|
29
|
-
import { and, asc, count, desc, eq, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
27
|
+
import { and, asc, count, desc, eq, gt, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
30
28
|
import * as PgDrizzle from "drizzle-orm/effect-postgres";
|
|
31
29
|
import { createInsertSchema, createSelectSchema } from "drizzle-orm/effect-schema";
|
|
32
30
|
import { alias, bigint, boolean, bytea, check, foreignKey, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
|
@@ -59,50 +57,6 @@ function activityEntryFromDraft(id, draft, occurredAt) {
|
|
|
59
57
|
});
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region ../server/src/internal/access-control.ts
|
|
64
|
-
const VIEWER_ACTIONS = /* @__PURE__ */ new Set([
|
|
65
|
-
"document.browse",
|
|
66
|
-
"document.read",
|
|
67
|
-
"document.trash",
|
|
68
|
-
"document.history",
|
|
69
|
-
"media.read",
|
|
70
|
-
"media.search",
|
|
71
|
-
"media.integrity"
|
|
72
|
-
]);
|
|
73
|
-
const EDITOR_ACTIONS = /* @__PURE__ */ new Set([...DOCUMENT_ACTIONS, ...MEDIA_ACTIONS.filter((action) => action !== "media.cleanup")]);
|
|
74
|
-
var AccessControl = class extends Context.Service()("@internal/server/AccessControl") {};
|
|
75
|
-
const defaultAccessControlLayer = Layer.succeed(AccessControl, AccessControl.of({ require: Effect.fn("AccessControl.require")(function* (context, decision) {
|
|
76
|
-
if (isAllowed(context, decision.action)) return;
|
|
77
|
-
return yield* new AccessDenied({
|
|
78
|
-
action: decision.action,
|
|
79
|
-
actorId: context.actor.id,
|
|
80
|
-
resource: decision.resource
|
|
81
|
-
});
|
|
82
|
-
}) }));
|
|
83
|
-
function isAllowed(context, action) {
|
|
84
|
-
return context.actor.roles.some((role) => {
|
|
85
|
-
if (role === "administrator") return true;
|
|
86
|
-
return role === "editor" && EDITOR_ACTIONS.has(action) || role === "viewer" && VIEWER_ACTIONS.has(action);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
//#endregion
|
|
91
|
-
//#region ../server/src/internal/cursor.ts
|
|
92
|
-
/** Owns the opaque base64url JSON representation shared by keyset cursors. */
|
|
93
|
-
function cursorCodec(schema, makeCursor, invalid) {
|
|
94
|
-
const decodeJson = Schema.decodeUnknownResult(schema);
|
|
95
|
-
const encodeJson = Schema.encodeSync(schema);
|
|
96
|
-
return {
|
|
97
|
-
decode: (cursor) => {
|
|
98
|
-
const decoded = Encoding.decodeBase64UrlString(cursor);
|
|
99
|
-
if (Result.isFailure(decoded)) return Result.fail(invalid());
|
|
100
|
-
return decodeJson(decoded.success).pipe(Result.mapError(invalid));
|
|
101
|
-
},
|
|
102
|
-
encode: (payload) => makeCursor(Encoding.encodeBase64Url(encodeJson(payload)))
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
60
|
//#endregion
|
|
107
61
|
//#region ../server/src/internal/activity/activity.ts
|
|
108
62
|
var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailable", {
|
|
@@ -110,11 +64,11 @@ var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailab
|
|
|
110
64
|
cause: Schema.Defect()
|
|
111
65
|
}) {};
|
|
112
66
|
var Activity = class extends Context.Service()("@internal/server/Activity") {};
|
|
113
|
-
const CursorPayload$
|
|
67
|
+
const CursorPayload$1 = Schema.Struct({
|
|
114
68
|
id: ActivityId,
|
|
115
69
|
occurredAt: Schema.DateTimeUtcFromString
|
|
116
70
|
});
|
|
117
|
-
const cursors$
|
|
71
|
+
const cursors$1 = cursorCodec(Schema.fromJsonString(CursorPayload$1), (value) => ActivityCursor.make(value), () => new ActivityCursorInvalid());
|
|
118
72
|
const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
|
|
119
73
|
const accessControl = yield* AccessControl;
|
|
120
74
|
const storage = yield* ActivityStore;
|
|
@@ -123,7 +77,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
|
|
|
123
77
|
action: "activity.read",
|
|
124
78
|
resource: { kind: "activity" }
|
|
125
79
|
});
|
|
126
|
-
const cursor = query.cursor ? yield* Effect.fromResult(cursors$
|
|
80
|
+
const cursor = query.cursor ? yield* Effect.fromResult(cursors$1.decode(query.cursor)) : void 0;
|
|
127
81
|
const page = yield* storage.list({
|
|
128
82
|
action: query.action,
|
|
129
83
|
actorId: query.actorId,
|
|
@@ -139,7 +93,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
|
|
|
139
93
|
};
|
|
140
94
|
return page.next ? ActivityPage.make({
|
|
141
95
|
...output,
|
|
142
|
-
nextCursor: cursors$
|
|
96
|
+
nextCursor: cursors$1.encode(page.next)
|
|
143
97
|
}) : ActivityPage.make(output);
|
|
144
98
|
});
|
|
145
99
|
return Activity.of({ list });
|
|
@@ -255,7 +209,6 @@ const ROUND = [
|
|
|
255
209
|
3204031479,
|
|
256
210
|
3329325298
|
|
257
211
|
];
|
|
258
|
-
/** Portable synchronous SHA-256 for deterministic journal fingerprints and secret redaction. */
|
|
259
212
|
function sha256Hex(value) {
|
|
260
213
|
const input = new TextEncoder().encode(value);
|
|
261
214
|
const bitLength = BigInt(input.length) * 8n;
|
|
@@ -335,14 +288,14 @@ const PendingChange = ChangeDraft.pipe(Schema.fieldsAssign({
|
|
|
335
288
|
updatedAt: Schema.DateTimeUtcFromString
|
|
336
289
|
}));
|
|
337
290
|
function changeFingerprint(change) {
|
|
338
|
-
const encoded = Schema.
|
|
291
|
+
const encoded = Result.getOrThrow(Schema.encodeResult(ChangeDraft)(change));
|
|
339
292
|
return sha256Hex(JSON.stringify(canonicalJson(encoded)));
|
|
340
293
|
}
|
|
341
|
-
const isJsonObject$
|
|
342
|
-
const decodeJson$2 = Schema.
|
|
294
|
+
const isJsonObject$4 = Schema.is(Schema.JsonObject);
|
|
295
|
+
const decodeJson$2 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
|
|
343
296
|
function canonicalJson(value) {
|
|
344
297
|
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
345
|
-
if (!isJsonObject$
|
|
298
|
+
if (!isJsonObject$4(value)) return value;
|
|
346
299
|
return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, canonicalJson(decodeJson$2(value[key]))]));
|
|
347
300
|
}
|
|
348
301
|
|
|
@@ -350,17 +303,17 @@ function canonicalJson(value) {
|
|
|
350
303
|
//#region ../server/src/internal/change-journal/change-journal.ts
|
|
351
304
|
const ChangeBegin = Data.taggedEnum();
|
|
352
305
|
function requireUnresolvedChange(result) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
changeId:
|
|
306
|
+
return ChangeBegin.$match(result, {
|
|
307
|
+
Unresolved: ({ change }) => Effect.succeed(change),
|
|
308
|
+
Recorded: ({ entry }) => Effect.fail(new ChangeAlreadyResolved({
|
|
309
|
+
changeId: entry.id,
|
|
357
310
|
outcome: "applied"
|
|
358
|
-
}))
|
|
359
|
-
|
|
360
|
-
changeId
|
|
311
|
+
})),
|
|
312
|
+
Cancelled: ({ changeId }) => Effect.fail(new ChangeAlreadyResolved({
|
|
313
|
+
changeId,
|
|
361
314
|
outcome: "cancelled"
|
|
362
|
-
}))
|
|
363
|
-
}
|
|
315
|
+
}))
|
|
316
|
+
});
|
|
364
317
|
}
|
|
365
318
|
var ChangeJournal = class extends Context.Service()("@internal/server/ChangeJournal") {};
|
|
366
319
|
const findPendingChange = Effect.fn("ChangeJournal.findPending")(function* (journal, subject) {
|
|
@@ -425,7 +378,7 @@ function initialAdministratorDraft(identity, intent) {
|
|
|
425
378
|
action: "user.created",
|
|
426
379
|
context: identity.context,
|
|
427
380
|
id: identity.id,
|
|
428
|
-
intent: Schema.
|
|
381
|
+
intent: Result.getOrThrow(Schema.encodeResult(InitialAdministratorIntent)(intent)),
|
|
429
382
|
scope: "initialization",
|
|
430
383
|
subject: userEmailSubject(intent.normalizedEmail)
|
|
431
384
|
});
|
|
@@ -567,7 +520,7 @@ function userChangeDraft(identity, intent) {
|
|
|
567
520
|
action: userChangeAction(intent),
|
|
568
521
|
context: identity.context,
|
|
569
522
|
id: identity.id,
|
|
570
|
-
intent: Schema.
|
|
523
|
+
intent: Result.getOrThrow(Schema.encodeResult(UserChangeIntent)(intent)),
|
|
571
524
|
scope: "users",
|
|
572
525
|
subject: userEmailSubject(intent.kind === "create" ? intent.normalizedEmail : intent.email)
|
|
573
526
|
};
|
|
@@ -733,12 +686,22 @@ function appliedResolution(change, intent, userId) {
|
|
|
733
686
|
//#region ../auth/src/internal/user.ts
|
|
734
687
|
const cmsUserCreationChangeField = "cmsCreationChangeId";
|
|
735
688
|
const cmsUserSessionsRevokedChangeField = "cmsSessionsRevokedChangeId";
|
|
689
|
+
const BetterAuthUser = Schema.Struct({
|
|
690
|
+
banned: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
|
|
691
|
+
createdAt: Schema.Date,
|
|
692
|
+
email: Schema.String,
|
|
693
|
+
emailVerified: Schema.Boolean,
|
|
694
|
+
id: Schema.String,
|
|
695
|
+
name: Schema.String,
|
|
696
|
+
role: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
697
|
+
updatedAt: Schema.Date
|
|
698
|
+
});
|
|
736
699
|
const BetterAuthUserRecoveryEvidence = Schema.Struct({
|
|
737
700
|
[cmsUserCreationChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId))),
|
|
738
701
|
[cmsUserSessionsRevokedChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId)))
|
|
739
702
|
});
|
|
740
|
-
function decodeBetterAuthUser(
|
|
741
|
-
return Schema.decodeUnknownEffect(User)({
|
|
703
|
+
function decodeBetterAuthUser(value) {
|
|
704
|
+
return Schema.decodeEffect(BetterAuthUser)(value).pipe(Effect.flatMap((user) => Schema.decodeUnknownEffect(User)({
|
|
742
705
|
banned: user.banned === true,
|
|
743
706
|
createdAt: user.createdAt.toISOString(),
|
|
744
707
|
email: user.email,
|
|
@@ -747,7 +710,7 @@ function decodeBetterAuthUser(user) {
|
|
|
747
710
|
name: user.name,
|
|
748
711
|
role: user.role,
|
|
749
712
|
updatedAt: user.updatedAt.toISOString()
|
|
750
|
-
});
|
|
713
|
+
})));
|
|
751
714
|
}
|
|
752
715
|
function decodeBetterAuthActor(user) {
|
|
753
716
|
return Schema.decodeEffect(StudioActor)(betterAuthActorInput(user));
|
|
@@ -784,12 +747,12 @@ function betterAuthActivityWriter(storage, crypto) {
|
|
|
784
747
|
return yield* storage.append(activityEntryFromDraft(id, draft, yield* DateTime.now));
|
|
785
748
|
});
|
|
786
749
|
}
|
|
787
|
-
function betterAuthActivityOptions(writeActivity, crypto) {
|
|
750
|
+
function betterAuthActivityOptions(writeActivity, crypto, runPromise) {
|
|
788
751
|
const capturedActors = /* @__PURE__ */ new WeakMap();
|
|
789
|
-
const write = (action, actor) =>
|
|
752
|
+
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))));
|
|
790
753
|
const before = createAuthMiddleware((context) => {
|
|
791
754
|
if (context.path !== "/sign-out" && context.path !== "/change-password") return Promise.resolve();
|
|
792
|
-
return
|
|
755
|
+
return runPromise(Effect.tryPromise(() => getSessionFromCtx(context, { disableRefresh: true })).pipe(Effect.orElseSucceed(() => null), Effect.tap((session) => Effect.sync(() => {
|
|
793
756
|
const actor = session?.user ? decodeActor(session.user) : void 0;
|
|
794
757
|
if (actor) capturedActors.set(context.context, actor);
|
|
795
758
|
})), Effect.asVoid));
|
|
@@ -833,7 +796,149 @@ function activityDraft(action, actor, requestId) {
|
|
|
833
796
|
}
|
|
834
797
|
function decodeActor(user) {
|
|
835
798
|
const option = decodeBetterAuthActorOption(user);
|
|
836
|
-
return
|
|
799
|
+
return Option.getOrUndefined(option);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region ../auth/src/internal/better-auth.ts
|
|
804
|
+
const cmsBetterAuthAccess = createAccessControl(defaultStatements);
|
|
805
|
+
const cmsBetterAuthRoles = {
|
|
806
|
+
administrator: cmsBetterAuthAccess.newRole({
|
|
807
|
+
session: ["list", "revoke"],
|
|
808
|
+
user: [
|
|
809
|
+
"create",
|
|
810
|
+
"list",
|
|
811
|
+
"get",
|
|
812
|
+
"set-role",
|
|
813
|
+
"delete",
|
|
814
|
+
"update"
|
|
815
|
+
]
|
|
816
|
+
}),
|
|
817
|
+
editor: cmsBetterAuthAccess.newRole({
|
|
818
|
+
session: [],
|
|
819
|
+
user: []
|
|
820
|
+
}),
|
|
821
|
+
viewer: cmsBetterAuthAccess.newRole({
|
|
822
|
+
session: [],
|
|
823
|
+
user: []
|
|
824
|
+
})
|
|
825
|
+
};
|
|
826
|
+
const timestampFields = {
|
|
827
|
+
createdAt: "created_at",
|
|
828
|
+
updatedAt: "updated_at"
|
|
829
|
+
};
|
|
830
|
+
function createCmsBetterAuthOptions(options, activity) {
|
|
831
|
+
return {
|
|
832
|
+
advanced: { database: {
|
|
833
|
+
generateId: "uuid",
|
|
834
|
+
joins: true
|
|
835
|
+
} },
|
|
836
|
+
appName: "CMS Studio",
|
|
837
|
+
basePath: options.basePath ?? "/api/cms/auth",
|
|
838
|
+
baseURL: options.baseURL,
|
|
839
|
+
database: options.database,
|
|
840
|
+
emailAndPassword: {
|
|
841
|
+
disableSignUp: true,
|
|
842
|
+
enabled: true,
|
|
843
|
+
...activity?.emailAndPassword
|
|
844
|
+
},
|
|
845
|
+
hooks: activity?.hooks,
|
|
846
|
+
user: {
|
|
847
|
+
additionalFields: {
|
|
848
|
+
[cmsUserCreationChangeField]: hiddenChangeField("cms_creation_change_id"),
|
|
849
|
+
[cmsUserSessionsRevokedChangeField]: hiddenChangeField("cms_sessions_revoked_change_id")
|
|
850
|
+
},
|
|
851
|
+
modelName: "auth_users",
|
|
852
|
+
fields: {
|
|
853
|
+
...timestampFields,
|
|
854
|
+
emailVerified: "email_verified"
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
session: {
|
|
858
|
+
cookieCache: {
|
|
859
|
+
enabled: false,
|
|
860
|
+
strategy: "jwt"
|
|
861
|
+
},
|
|
862
|
+
modelName: "auth_sessions",
|
|
863
|
+
fields: {
|
|
864
|
+
...timestampFields,
|
|
865
|
+
expiresAt: "expires_at",
|
|
866
|
+
ipAddress: "ip_address",
|
|
867
|
+
userAgent: "user_agent",
|
|
868
|
+
userId: "user_id"
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
account: {
|
|
872
|
+
modelName: "auth_accounts",
|
|
873
|
+
fields: {
|
|
874
|
+
accessToken: "access_token",
|
|
875
|
+
accessTokenExpiresAt: "access_token_expires_at",
|
|
876
|
+
accountId: "account_id",
|
|
877
|
+
...timestampFields,
|
|
878
|
+
idToken: "id_token",
|
|
879
|
+
providerId: "provider_id",
|
|
880
|
+
refreshToken: "refresh_token",
|
|
881
|
+
refreshTokenExpiresAt: "refresh_token_expires_at",
|
|
882
|
+
userId: "user_id"
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
verification: {
|
|
886
|
+
modelName: "auth_verifications",
|
|
887
|
+
fields: {
|
|
888
|
+
...timestampFields,
|
|
889
|
+
expiresAt: "expires_at"
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
rateLimit: {
|
|
893
|
+
enabled: true,
|
|
894
|
+
storage: "database",
|
|
895
|
+
modelName: "auth_rate_limits",
|
|
896
|
+
fields: { lastRequest: "last_request" }
|
|
897
|
+
},
|
|
898
|
+
plugins: [admin({
|
|
899
|
+
ac: cmsBetterAuthAccess,
|
|
900
|
+
adminRoles: ["administrator"],
|
|
901
|
+
defaultRole: "editor",
|
|
902
|
+
roles: cmsBetterAuthRoles,
|
|
903
|
+
schema: {
|
|
904
|
+
session: { fields: { impersonatedBy: "impersonated_by" } },
|
|
905
|
+
user: { fields: {
|
|
906
|
+
banExpires: "ban_expires",
|
|
907
|
+
banReason: "ban_reason",
|
|
908
|
+
banned: "banned",
|
|
909
|
+
role: "role"
|
|
910
|
+
} }
|
|
911
|
+
}
|
|
912
|
+
})],
|
|
913
|
+
secret: options.secret
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function createCmsBetterAuth(options, activity) {
|
|
917
|
+
return betterAuth(createCmsBetterAuthOptions(options, activity));
|
|
918
|
+
}
|
|
919
|
+
function hiddenChangeField(fieldName) {
|
|
920
|
+
return {
|
|
921
|
+
fieldName,
|
|
922
|
+
input: false,
|
|
923
|
+
required: false,
|
|
924
|
+
returned: false,
|
|
925
|
+
type: "string"
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region ../auth/src/internal/handler.ts
|
|
931
|
+
function rejectExternalAdminRoutes(handler, basePath) {
|
|
932
|
+
const mountedAt = canonicalBasePath(basePath);
|
|
933
|
+
return (request) => {
|
|
934
|
+
const path = new URL(request.url).pathname;
|
|
935
|
+
const relative = mountedAt === "/" ? path : path === mountedAt ? "/" : path.startsWith(`${mountedAt}/`) ? path.slice(mountedAt.length) : path;
|
|
936
|
+
if (relative === "/admin" || relative.startsWith("/admin/")) return Promise.resolve(new Response(null, { status: 404 }));
|
|
937
|
+
return handler(request);
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function canonicalBasePath(basePath) {
|
|
941
|
+
return (basePath.startsWith("/") ? basePath : `/${basePath}`).replace(/\/+$/u, "") || "/";
|
|
837
942
|
}
|
|
838
943
|
|
|
839
944
|
//#endregion
|
|
@@ -916,9 +1021,24 @@ function matches(code, ...candidates) {
|
|
|
916
1021
|
return code !== void 0 && candidates.some((candidate) => candidate.code === code);
|
|
917
1022
|
}
|
|
918
1023
|
function apiErrorCode(cause) {
|
|
919
|
-
if (!(cause instanceof APIError)
|
|
920
|
-
const
|
|
921
|
-
|
|
1024
|
+
if (!(cause instanceof Error) || cause.name !== "APIError") return void 0;
|
|
1025
|
+
const apiError = Schema.decodeUnknownOption(Schema.Struct({
|
|
1026
|
+
body: Schema.Struct({ code: Schema.String }),
|
|
1027
|
+
statusCode: Schema.Int
|
|
1028
|
+
}))(cause);
|
|
1029
|
+
return Option.map(apiError, ({ body }) => body.code).pipe(Option.getOrUndefined);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region ../auth/src/internal/provider.ts
|
|
1034
|
+
function providerCall(operation, request, onError) {
|
|
1035
|
+
return Effect.tryPromise({
|
|
1036
|
+
catch: (cause) => onError(operation, cause),
|
|
1037
|
+
try: request
|
|
1038
|
+
}).pipe(Effect.withSpan(operation));
|
|
1039
|
+
}
|
|
1040
|
+
function providerAdapter(auth, operation, onError) {
|
|
1041
|
+
return providerCall(operation, () => auth.$context, onError).pipe(Effect.map((context) => context.internalAdapter));
|
|
922
1042
|
}
|
|
923
1043
|
|
|
924
1044
|
//#endregion
|
|
@@ -927,13 +1047,10 @@ function betterAuthIdentity(auth) {
|
|
|
927
1047
|
const authorityHeaders = /* @__PURE__ */ new WeakMap();
|
|
928
1048
|
const authenticate = Effect.fn("BetterAuth.Authentication.authenticate")(function* (headers) {
|
|
929
1049
|
const providerHeaders = new globalThis.Headers(headers);
|
|
930
|
-
const session = yield*
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
query: { disableCookieCache: true }
|
|
935
|
-
})
|
|
936
|
-
});
|
|
1050
|
+
const session = yield* providerCall("BetterAuth.getSession", () => auth.api.getSession({
|
|
1051
|
+
headers: providerHeaders,
|
|
1052
|
+
query: { disableCookieCache: true }
|
|
1053
|
+
}), () => studioInternalError());
|
|
937
1054
|
if (!session?.user) return null;
|
|
938
1055
|
const actor = yield* decodeBetterAuthActor(session.user).pipe(Effect.mapError(() => studioInternalError()));
|
|
939
1056
|
const authority = issueIdentityAuthority();
|
|
@@ -1027,16 +1144,13 @@ function betterAuthIdentity(auth) {
|
|
|
1027
1144
|
const apply = (changeId) => applyOperation(auth, "BetterAuth.revokeUserSessions", command, () => auth.api.revokeUserSessions({
|
|
1028
1145
|
body: { userId: command.userId },
|
|
1029
1146
|
headers
|
|
1030
|
-
})).pipe(Effect.flatMap(() =>
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
headers
|
|
1038
|
-
})
|
|
1039
|
-
})), Effect.as(user));
|
|
1147
|
+
})).pipe(Effect.flatMap(() => providerCall("BetterAuth.adminUpdateUser.sessionRevocationReceipt", () => auth.api.adminUpdateUser({
|
|
1148
|
+
body: {
|
|
1149
|
+
data: { [cmsUserSessionsRevokedChangeField]: changeId },
|
|
1150
|
+
userId: command.userId
|
|
1151
|
+
},
|
|
1152
|
+
headers
|
|
1153
|
+
}), (operation, cause) => IdentityApplyFailure.Unknown({ error: unavailable$4(operation, cause) }))), Effect.as(user));
|
|
1040
1154
|
return IdentityPreparation.Prepared({
|
|
1041
1155
|
apply,
|
|
1042
1156
|
intent,
|
|
@@ -1054,22 +1168,13 @@ function betterAuthIdentity(auth) {
|
|
|
1054
1168
|
};
|
|
1055
1169
|
}
|
|
1056
1170
|
function prepareOperation(auth, operation, request) {
|
|
1057
|
-
return
|
|
1058
|
-
catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause),
|
|
1059
|
-
try: request
|
|
1060
|
-
});
|
|
1171
|
+
return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause));
|
|
1061
1172
|
}
|
|
1062
1173
|
function prepareUserOperation(auth, operation, userId, request) {
|
|
1063
|
-
return
|
|
1064
|
-
catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause, userId),
|
|
1065
|
-
try: request
|
|
1066
|
-
});
|
|
1174
|
+
return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause, userId));
|
|
1067
1175
|
}
|
|
1068
1176
|
function applyOperation(auth, operation, command, request) {
|
|
1069
|
-
return
|
|
1070
|
-
catch: (cause) => applyError(auth.$ERROR_CODES, operation, command, cause),
|
|
1071
|
-
try: request
|
|
1072
|
-
});
|
|
1177
|
+
return providerCall(operation, request, (name, cause) => applyError(auth.$ERROR_CODES, name, command, cause));
|
|
1073
1178
|
}
|
|
1074
1179
|
function requireAuthority(authorityHeaders, authority) {
|
|
1075
1180
|
const headers = authorityHeaders.get(authority);
|
|
@@ -1107,13 +1212,10 @@ function betterAuthInitialIdentity(auth) {
|
|
|
1107
1212
|
});
|
|
1108
1213
|
const intent = initialAdministratorIntent(input);
|
|
1109
1214
|
return {
|
|
1110
|
-
apply:
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
...createInput
|
|
1115
|
-
} })
|
|
1116
|
-
}).pipe(Effect.flatMap((result) => decodeBetterAuthUser(result.user).pipe(Effect.mapError((cause) => InitialIdentityFailure.Unknown({ error: initializationUnavailable$1("BetterAuth.initialization.createUser", cause) }))))),
|
|
1215
|
+
apply: providerCall("BetterAuth.initialization.createUser", () => auth.api.createUser({ body: {
|
|
1216
|
+
data: { emailVerified: true },
|
|
1217
|
+
...createInput
|
|
1218
|
+
} }), (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) }))))),
|
|
1117
1219
|
intent
|
|
1118
1220
|
};
|
|
1119
1221
|
} });
|
|
@@ -1122,1399 +1224,227 @@ function betterAuthInitialIdentity(auth) {
|
|
|
1122
1224
|
//#endregion
|
|
1123
1225
|
//#region ../auth/src/internal/recovery.ts
|
|
1124
1226
|
function betterAuthRecovery(auth) {
|
|
1125
|
-
const userAdapter =
|
|
1126
|
-
|
|
1127
|
-
try: () => auth.$context
|
|
1128
|
-
}).pipe(Effect.map((context) => context.internalAdapter));
|
|
1129
|
-
const initialAdapter = Effect.tryPromise({
|
|
1130
|
-
catch: (cause) => initializationUnavailable$1("BetterAuth.initialization.recovery.context", cause),
|
|
1131
|
-
try: () => auth.$context
|
|
1132
|
-
}).pipe(Effect.map((context) => context.internalAdapter));
|
|
1227
|
+
const userAdapter = providerAdapter(auth, "BetterAuth.recovery.context", unavailable$4);
|
|
1228
|
+
const initialAdapter = providerAdapter(auth, "BetterAuth.initialization.recovery.context", initializationUnavailable$1);
|
|
1133
1229
|
const users = IdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeUserChange")(function* (intent) {
|
|
1134
1230
|
const adapter = yield* userAdapter;
|
|
1135
1231
|
switch (intent.kind) {
|
|
1136
1232
|
case "create": {
|
|
1137
|
-
const result = yield*
|
|
1138
|
-
catch: (cause) => unavailable$4("BetterAuth.recovery.findUserByEmail", cause),
|
|
1139
|
-
try: () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true })
|
|
1140
|
-
});
|
|
1233
|
+
const result = yield* providerCall("BetterAuth.recovery.findUserByEmail", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), unavailable$4);
|
|
1141
1234
|
if (!result) return {
|
|
1142
1235
|
creationChangeId: null,
|
|
1143
1236
|
credentialReady: false,
|
|
1144
1237
|
kind: "create",
|
|
1145
1238
|
user: null
|
|
1146
1239
|
};
|
|
1147
|
-
const user = yield* decodeUser(result.user, "BetterAuth.recovery.findUserByEmail");
|
|
1148
|
-
return {
|
|
1149
|
-
creationChangeId: yield* decodeBetterAuthUserCreationChangeId(result.user).pipe(Effect.mapError((cause) => unavailable$4("BetterAuth.recovery.creationChangeId", cause))),
|
|
1150
|
-
credentialReady: hasCredential(result.accounts),
|
|
1151
|
-
kind: "create",
|
|
1152
|
-
user
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
case "remove": {
|
|
1156
|
-
const user = yield* findUser(adapter, intent.userId);
|
|
1157
|
-
return {
|
|
1158
|
-
kind: "remove",
|
|
1159
|
-
sessionFingerprints: yield* findSessionFingerprints(adapter, intent.userId),
|
|
1160
|
-
user
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
case "change-role": return {
|
|
1164
|
-
kind: "change-role",
|
|
1165
|
-
user: yield* findUser(adapter, intent.userId)
|
|
1166
|
-
};
|
|
1167
|
-
case "revoke-sessions": {
|
|
1168
|
-
const rawUser = yield* findRawUser(adapter, intent.userId);
|
|
1169
|
-
const user = rawUser ? yield* decodeUser(rawUser, "BetterAuth.recovery.findUserById") : null;
|
|
1170
|
-
return {
|
|
1171
|
-
kind: "revoke-sessions",
|
|
1172
|
-
sessionFingerprints: yield* findSessionFingerprints(adapter, intent.userId),
|
|
1173
|
-
sessionsRevokedChangeId: rawUser ? yield* decodeBetterAuthUserSessionsRevokedChangeId(rawUser).pipe(Effect.mapError((cause) => unavailable$4("BetterAuth.recovery.sessionsRevokedChangeId", cause))) : null,
|
|
1174
|
-
user
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}) });
|
|
1179
|
-
return {
|
|
1180
|
-
initial: InitialIdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeInitialAdministrator")(function* (intent) {
|
|
1181
|
-
const adapter = yield* initialAdapter;
|
|
1182
|
-
const result = yield* Effect.tryPromise({
|
|
1183
|
-
catch: (cause) => initializationUnavailable$1("BetterAuth.recovery.findInitialAdministrator", cause),
|
|
1184
|
-
try: () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true })
|
|
1185
|
-
});
|
|
1186
|
-
if (!result) return {
|
|
1187
|
-
credentialReady: false,
|
|
1188
|
-
user: null
|
|
1189
|
-
};
|
|
1190
|
-
return {
|
|
1191
|
-
credentialReady: hasCredential(result.accounts),
|
|
1192
|
-
user: yield* decodeBetterAuthUser(result.user).pipe(Effect.mapError((cause) => initializationUnavailable$1("BetterAuth.recovery.findInitialAdministrator", cause)))
|
|
1193
|
-
};
|
|
1194
|
-
}) }),
|
|
1195
|
-
users
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
function hasCredential(accounts) {
|
|
1199
|
-
return accounts.some((account) => account.providerId === "credential" && (account.password?.length ?? 0) > 0);
|
|
1200
|
-
}
|
|
1201
|
-
function decodeUser(user, operation) {
|
|
1202
|
-
return decodeBetterAuthUser(user).pipe(Effect.mapError((cause) => unavailable$4(operation, cause)));
|
|
1203
|
-
}
|
|
1204
|
-
function findUser(adapter, userId) {
|
|
1205
|
-
return findRawUser(adapter, userId).pipe(Effect.flatMap((user) => user ? decodeUser(user, "BetterAuth.recovery.findUserById") : Effect.succeed(null)));
|
|
1206
|
-
}
|
|
1207
|
-
function findRawUser(adapter, userId) {
|
|
1208
|
-
return Effect.tryPromise({
|
|
1209
|
-
catch: (cause) => unavailable$4("BetterAuth.recovery.findUserById", cause),
|
|
1210
|
-
try: () => adapter.findUserById(userId)
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
function findSessionFingerprints(adapter, userId) {
|
|
1214
|
-
return Effect.tryPromise({
|
|
1215
|
-
catch: (cause) => unavailable$4("BetterAuth.recovery.listSessions", cause),
|
|
1216
|
-
try: () => adapter.listSessions(userId)
|
|
1217
|
-
}).pipe(Effect.map((sessions) => sessions.map((session) => sessionFingerprint(session.id))));
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
//#endregion
|
|
1221
|
-
//#region ../auth/src/internal/better-auth.ts
|
|
1222
|
-
const cmsBetterAuthAccess = createAccessControl(defaultStatements);
|
|
1223
|
-
const cmsBetterAuthRoles = {
|
|
1224
|
-
administrator: cmsBetterAuthAccess.newRole({
|
|
1225
|
-
session: ["list", "revoke"],
|
|
1226
|
-
user: [
|
|
1227
|
-
"create",
|
|
1228
|
-
"list",
|
|
1229
|
-
"get",
|
|
1230
|
-
"set-role",
|
|
1231
|
-
"delete",
|
|
1232
|
-
"update"
|
|
1233
|
-
]
|
|
1234
|
-
}),
|
|
1235
|
-
editor: cmsBetterAuthAccess.newRole({
|
|
1236
|
-
session: [],
|
|
1237
|
-
user: []
|
|
1238
|
-
}),
|
|
1239
|
-
viewer: cmsBetterAuthAccess.newRole({
|
|
1240
|
-
session: [],
|
|
1241
|
-
user: []
|
|
1242
|
-
})
|
|
1243
|
-
};
|
|
1244
|
-
const timestampFields = {
|
|
1245
|
-
createdAt: "created_at",
|
|
1246
|
-
updatedAt: "updated_at"
|
|
1247
|
-
};
|
|
1248
|
-
function createCmsBetterAuthOptions(options, activity) {
|
|
1249
|
-
return {
|
|
1250
|
-
advanced: { database: {
|
|
1251
|
-
generateId: "uuid",
|
|
1252
|
-
joins: true
|
|
1253
|
-
} },
|
|
1254
|
-
appName: "CMS Studio",
|
|
1255
|
-
basePath: options.basePath ?? "/api/cms/auth",
|
|
1256
|
-
baseURL: options.baseURL,
|
|
1257
|
-
database: options.database,
|
|
1258
|
-
emailAndPassword: {
|
|
1259
|
-
disableSignUp: true,
|
|
1260
|
-
enabled: true,
|
|
1261
|
-
...activity?.emailAndPassword
|
|
1262
|
-
},
|
|
1263
|
-
hooks: activity?.hooks,
|
|
1264
|
-
user: {
|
|
1265
|
-
additionalFields: {
|
|
1266
|
-
[cmsUserCreationChangeField]: hiddenChangeField("cms_creation_change_id"),
|
|
1267
|
-
[cmsUserSessionsRevokedChangeField]: hiddenChangeField("cms_sessions_revoked_change_id")
|
|
1268
|
-
},
|
|
1269
|
-
modelName: "auth_users",
|
|
1270
|
-
fields: {
|
|
1271
|
-
...timestampFields,
|
|
1272
|
-
emailVerified: "email_verified"
|
|
1273
|
-
}
|
|
1274
|
-
},
|
|
1275
|
-
session: {
|
|
1276
|
-
cookieCache: {
|
|
1277
|
-
enabled: false,
|
|
1278
|
-
strategy: "jwt"
|
|
1279
|
-
},
|
|
1280
|
-
modelName: "auth_sessions",
|
|
1281
|
-
fields: {
|
|
1282
|
-
...timestampFields,
|
|
1283
|
-
expiresAt: "expires_at",
|
|
1284
|
-
ipAddress: "ip_address",
|
|
1285
|
-
userAgent: "user_agent",
|
|
1286
|
-
userId: "user_id"
|
|
1287
|
-
}
|
|
1288
|
-
},
|
|
1289
|
-
account: {
|
|
1290
|
-
modelName: "auth_accounts",
|
|
1291
|
-
fields: {
|
|
1292
|
-
accessToken: "access_token",
|
|
1293
|
-
accessTokenExpiresAt: "access_token_expires_at",
|
|
1294
|
-
accountId: "account_id",
|
|
1295
|
-
...timestampFields,
|
|
1296
|
-
idToken: "id_token",
|
|
1297
|
-
providerId: "provider_id",
|
|
1298
|
-
refreshToken: "refresh_token",
|
|
1299
|
-
refreshTokenExpiresAt: "refresh_token_expires_at",
|
|
1300
|
-
userId: "user_id"
|
|
1301
|
-
}
|
|
1302
|
-
},
|
|
1303
|
-
verification: {
|
|
1304
|
-
modelName: "auth_verifications",
|
|
1305
|
-
fields: {
|
|
1306
|
-
...timestampFields,
|
|
1307
|
-
expiresAt: "expires_at"
|
|
1308
|
-
}
|
|
1309
|
-
},
|
|
1310
|
-
rateLimit: {
|
|
1311
|
-
enabled: true,
|
|
1312
|
-
storage: "database",
|
|
1313
|
-
modelName: "auth_rate_limits",
|
|
1314
|
-
fields: { lastRequest: "last_request" }
|
|
1315
|
-
},
|
|
1316
|
-
plugins: [admin({
|
|
1317
|
-
ac: cmsBetterAuthAccess,
|
|
1318
|
-
adminRoles: ["administrator"],
|
|
1319
|
-
defaultRole: "editor",
|
|
1320
|
-
roles: cmsBetterAuthRoles,
|
|
1321
|
-
schema: {
|
|
1322
|
-
session: { fields: { impersonatedBy: "impersonated_by" } },
|
|
1323
|
-
user: { fields: {
|
|
1324
|
-
banExpires: "ban_expires",
|
|
1325
|
-
banReason: "ban_reason",
|
|
1326
|
-
banned: "banned",
|
|
1327
|
-
role: "role"
|
|
1328
|
-
} }
|
|
1329
|
-
}
|
|
1330
|
-
})],
|
|
1331
|
-
secret: options.secret
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
function createCmsBetterAuth(options, activity) {
|
|
1335
|
-
return betterAuth(createCmsBetterAuthOptions(options, activity));
|
|
1336
|
-
}
|
|
1337
|
-
function betterAuthLayer(options) {
|
|
1338
|
-
return Layer.effectContext(Effect.gen(function* () {
|
|
1339
|
-
const crypto = yield* Crypto.Crypto;
|
|
1340
|
-
const storage = yield* ActivityStore;
|
|
1341
|
-
const auth = createCmsBetterAuth(options, betterAuthActivityOptions(betterAuthActivityWriter(storage, crypto), crypto));
|
|
1342
|
-
const identity = betterAuthIdentity(auth);
|
|
1343
|
-
const recovery = betterAuthRecovery(auth);
|
|
1344
|
-
const initialization = betterAuthInitialIdentity(auth);
|
|
1345
|
-
const basePath = options.basePath ?? "/api/cms/auth";
|
|
1346
|
-
const handler = AuthenticationHandler.of({ handle: rejectExternalAdminRoutes(auth.handler, basePath) });
|
|
1347
|
-
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));
|
|
1348
|
-
}));
|
|
1349
|
-
}
|
|
1350
|
-
function rejectExternalAdminRoutes(handler, basePath) {
|
|
1351
|
-
const mountedAt = canonicalBasePath(basePath);
|
|
1352
|
-
return (request) => {
|
|
1353
|
-
const path = new URL(request.url).pathname;
|
|
1354
|
-
const relative = mountedAt === "/" ? path : path === mountedAt ? "/" : path.startsWith(`${mountedAt}/`) ? path.slice(mountedAt.length) : path;
|
|
1355
|
-
if (relative === "/admin" || relative.startsWith("/admin/")) return Promise.resolve(new Response(null, { status: 404 }));
|
|
1356
|
-
return handler(request);
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
function canonicalBasePath(basePath) {
|
|
1360
|
-
return (basePath.startsWith("/") ? basePath : `/${basePath}`).replace(/\/+$/u, "") || "/";
|
|
1361
|
-
}
|
|
1362
|
-
function hiddenChangeField(fieldName) {
|
|
1363
|
-
return {
|
|
1364
|
-
fieldName,
|
|
1365
|
-
input: false,
|
|
1366
|
-
required: false,
|
|
1367
|
-
returned: false,
|
|
1368
|
-
type: "string"
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
//#endregion
|
|
1373
|
-
//#region ../server/src/adapter/Readiness.ts
|
|
1374
|
-
var ReadinessFailed = class extends Schema.TaggedError()("Cms.ReadinessFailed", { cause: Schema.Defect() }) {};
|
|
1375
|
-
var Readiness = class extends Context.Service()("@internal/server/Readiness") {};
|
|
1376
|
-
|
|
1377
|
-
//#endregion
|
|
1378
|
-
//#region ../server/src/internal/working-documents/working-document-model.ts
|
|
1379
|
-
const WorkingDocumentGenerationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("WorkingDocumentGenerationId"));
|
|
1380
|
-
const WorkingDocumentUpdateSequence = Schema.Int.check(Schema.isGreaterThan(0)).pipe(Schema.brand("WorkingDocumentUpdateSequence"));
|
|
1381
|
-
const WorkingDocumentHighWatermark = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("WorkingDocumentHighWatermark"));
|
|
1382
|
-
var WorkingDocumentPersistenceError = class extends Schema.TaggedError()("WorkingDocuments.PersistenceError", {
|
|
1383
|
-
operation: Schema.NonEmptyString,
|
|
1384
|
-
cause: Schema.Defect()
|
|
1385
|
-
}) {};
|
|
1386
|
-
var WorkingDocumentSuperseded = class extends Schema.TaggedError()("WorkingDocuments.Superseded", {
|
|
1387
|
-
documentId: DocumentId,
|
|
1388
|
-
expectedGeneration: WorkingDocumentGenerationId,
|
|
1389
|
-
actualGeneration: Schema.optionalKey(WorkingDocumentGenerationId)
|
|
1390
|
-
}) {};
|
|
1391
|
-
var WorkingDocumentConflict = class extends Schema.TaggedError()("WorkingDocuments.Conflict", {
|
|
1392
|
-
documentId: DocumentId,
|
|
1393
|
-
operation: Schema.NonEmptyString,
|
|
1394
|
-
message: Schema.NonEmptyString
|
|
1395
|
-
}) {};
|
|
1396
|
-
|
|
1397
|
-
//#endregion
|
|
1398
|
-
//#region ../server/src/internal/working-documents/working-document.ts
|
|
1399
|
-
/** Durable identity for one exact recoverable Working Document generation. */
|
|
1400
|
-
const WorkingDocumentRef = Schema.Struct({
|
|
1401
|
-
documentId: DocumentId,
|
|
1402
|
-
generationId: WorkingDocumentGenerationId
|
|
1403
|
-
});
|
|
1404
|
-
var WorkingDocumentInvalid = class extends Schema.TaggedError()("WorkingDocuments.Invalid", {
|
|
1405
|
-
documentId: DocumentId,
|
|
1406
|
-
message: Schema.NonEmptyString,
|
|
1407
|
-
cause: Schema.Defect()
|
|
1408
|
-
}) {};
|
|
1409
|
-
var WorkingDocumentUnavailable = class extends Schema.TaggedError()("WorkingDocuments.Unavailable", {
|
|
1410
|
-
operation: Schema.NonEmptyString,
|
|
1411
|
-
cause: Schema.Defect()
|
|
1412
|
-
}) {};
|
|
1413
|
-
/** An authored or working baseline committed, but the publish workflow still needs recovery. */
|
|
1414
|
-
var WorkingDocumentCheckpointed = class extends Schema.TaggedError()("WorkingDocuments.Checkpointed", {
|
|
1415
|
-
checkpointVersion: DocumentVersion,
|
|
1416
|
-
documentId: DocumentId,
|
|
1417
|
-
result: Schema.optionalKey(DocumentMutationResult),
|
|
1418
|
-
stateVector: Schema.Uint8Array,
|
|
1419
|
-
cause: Schema.Defect()
|
|
1420
|
-
}) {};
|
|
1421
|
-
/** Signals that one exact Working Document generation changed or became invalid. */
|
|
1422
|
-
const WorkingDocumentChange = Schema.Struct({
|
|
1423
|
-
kind: Schema.Literals(["updated", "invalidated"]),
|
|
1424
|
-
ref: WorkingDocumentRef
|
|
1425
|
-
});
|
|
1426
|
-
var WorkingDocuments = class extends Context.Service()("@internal/server/WorkingDocuments") {};
|
|
1427
|
-
|
|
1428
|
-
//#endregion
|
|
1429
|
-
//#region ../document/src/internal/editor/render.ts
|
|
1430
|
-
const renderEditorMarkdown = Effect.fn("EditorDocument.renderMarkdown")((document) => {
|
|
1431
|
-
return Effect.tryPromise({
|
|
1432
|
-
try: () => renderMarkdown(toComarkDocument(withoutEditorMetadata(document))),
|
|
1433
|
-
catch: (cause) => new EditorRenderFailed({ cause })
|
|
1434
|
-
});
|
|
1435
|
-
});
|
|
1436
|
-
|
|
1437
|
-
//#endregion
|
|
1438
|
-
//#region ../document/src/DocumentPlanning.ts
|
|
1439
|
-
/** Derives one canonical Source path and public path without performing persistence. */
|
|
1440
|
-
function planDocumentCreation(configuration, input) {
|
|
1441
|
-
return Result.gen(function* () {
|
|
1442
|
-
const collection = configuration.types.find((candidate) => candidate.collection === input.collection);
|
|
1443
|
-
if (!collection) return yield* invalid$1(`Document collection "${input.collection}" does not exist.`, "collection");
|
|
1444
|
-
if (!collection.canCreate) return yield* invalid$1(`Document collection "${input.collection}" does not allow creation.`, "collection");
|
|
1445
|
-
const title = input.title.trim();
|
|
1446
|
-
if (!title) return yield* invalid$1("A document title is required.", "title");
|
|
1447
|
-
const locale = yield* resolveLocale(configuration, input.locale);
|
|
1448
|
-
const address = yield* parseDocumentAddress(collection.addressPattern).pipe(Result.mapError(addressInvalid));
|
|
1449
|
-
const relativePath = yield* resolveRelativePath(collection, input.location);
|
|
1450
|
-
const data = Object.fromEntries(address.tokens.flatMap((token) => {
|
|
1451
|
-
if (token.name === "slug" && relativePath) return [[token.name, relativePath.split("/").at(-1) ?? relativePath]];
|
|
1452
|
-
return [];
|
|
1453
|
-
}));
|
|
1454
|
-
const relativeKey = yield* normalizePublicPath(relativePath).pipe(Result.mapError(addressInvalid));
|
|
1455
|
-
const key = yield* resolveDocumentAddress(address, relativeKey, data).pipe(Result.mapError(addressInvalid));
|
|
1456
|
-
const sourcePath = documentSourcePath(collection, relativePath, locale);
|
|
1457
|
-
const path = localizeDocumentPath(key, locale, configuration.locales);
|
|
1458
|
-
return Object.assign({
|
|
1459
|
-
collection,
|
|
1460
|
-
data,
|
|
1461
|
-
key,
|
|
1462
|
-
path,
|
|
1463
|
-
sourcePath,
|
|
1464
|
-
title
|
|
1465
|
-
}, locale === void 0 ? void 0 : { locale });
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
/** Creates the canonical initial editor source, including configured field defaults. */
|
|
1469
|
-
const createInitialDocumentSource = Effect.fn("DocumentPlanning.createInitialSource")(function* (plan) {
|
|
1470
|
-
const body = CmsMarkdownDocument.make({
|
|
1471
|
-
frontmatter: initialFrontmatter(plan),
|
|
1472
|
-
meta: {},
|
|
1473
|
-
nodes: [[
|
|
1474
|
-
"h1",
|
|
1475
|
-
{},
|
|
1476
|
-
plan.title
|
|
1477
|
-
]]
|
|
1478
|
-
});
|
|
1479
|
-
return {
|
|
1480
|
-
body,
|
|
1481
|
-
markdown: yield* renderEditorMarkdown(body)
|
|
1482
|
-
};
|
|
1483
|
-
});
|
|
1484
|
-
/** Derives the canonical destination Source path while preserving the Document's collection and locale. */
|
|
1485
|
-
function planDocumentMove(configuration, document, location) {
|
|
1486
|
-
return Result.gen(function* () {
|
|
1487
|
-
const collection = configuration.types.find((candidate) => candidate.collection === document.collection);
|
|
1488
|
-
if (!collection) return yield* invalid$1(`Document collection "${document.collection}" does not exist.`, "documentId");
|
|
1489
|
-
return documentSourcePath(collection, yield* resolveRelativePath(collection, location), document.locale);
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
function documentSourcePath(collection, relativePath, locale) {
|
|
1493
|
-
const sourceName = relativePath ? `${relativePath}.md` : "index.md";
|
|
1494
|
-
return SourcePath.make([
|
|
1495
|
-
locale,
|
|
1496
|
-
collection.directory === "." ? void 0 : collection.directory,
|
|
1497
|
-
sourceName
|
|
1498
|
-
].filter((value) => value !== void 0).join("/"));
|
|
1499
|
-
}
|
|
1500
|
-
function initialFrontmatter(plan) {
|
|
1501
|
-
let frontmatter = {
|
|
1502
|
-
...plan.data,
|
|
1503
|
-
title: plan.title
|
|
1504
|
-
};
|
|
1505
|
-
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);
|
|
1506
|
-
return frontmatter;
|
|
1507
|
-
}
|
|
1508
|
-
function nestedValue$1(value, path) {
|
|
1509
|
-
return path.split(".").reduce((current, segment) => isJsonObject$4(current) ? current[segment] : void 0, value);
|
|
1510
|
-
}
|
|
1511
|
-
function setNestedValue(target, path, value) {
|
|
1512
|
-
const [segment, ...rest] = path;
|
|
1513
|
-
if (!segment) return target;
|
|
1514
|
-
if (rest.length === 0) return Object.assign({}, target, { [segment]: value });
|
|
1515
|
-
const current = target[segment];
|
|
1516
|
-
return Object.assign({}, target, { [segment]: setNestedValue(isJsonObject$4(current) ? current : {}, rest, value) });
|
|
1517
|
-
}
|
|
1518
|
-
const isJsonObject$4 = Schema.is(Schema.JsonObject);
|
|
1519
|
-
function resolveLocale(configuration, requested) {
|
|
1520
|
-
const locales = configuration.locales;
|
|
1521
|
-
if (!locales) return requested === void 0 ? Result.succeed(void 0) : invalid$1(`Document locale "${requested}" is not configured.`, "locale");
|
|
1522
|
-
const locale = requested ?? locales.defaultLocale;
|
|
1523
|
-
return locales.locales.includes(locale) ? Result.succeed(locale) : invalid$1(`Document locale "${locale}" does not exist.`, "locale");
|
|
1524
|
-
}
|
|
1525
|
-
function resolveRelativePath(collection, location) {
|
|
1526
|
-
if (location.kind === "home") return collection.addressPattern === "/:path*" ? Result.succeed("") : invalid$1("Only a root catch-all collection can use the home location.", "location");
|
|
1527
|
-
if (location.parent !== void 0 && !collection.hierarchy) return invalid$1(`Document collection "${collection.collection}" does not allow hierarchy.`, "location.parent");
|
|
1528
|
-
return normalizeDocumentRelativePath([location.parent, location.slug].filter((value) => value !== void 0).join("/")).pipe(Result.mapError((error) => addressInvalid({
|
|
1529
|
-
...error,
|
|
1530
|
-
field: "location"
|
|
1531
|
-
})));
|
|
1532
|
-
}
|
|
1533
|
-
function addressInvalid(error) {
|
|
1534
|
-
return new DocumentValidationFailed({ issues: [validationIssue(error.message, error.field)] });
|
|
1535
|
-
}
|
|
1536
|
-
function invalid$1(message, field) {
|
|
1537
|
-
return Result.fail(new DocumentValidationFailed({ issues: [validationIssue(message, field)] }));
|
|
1538
|
-
}
|
|
1539
|
-
function validationIssue(message, field) {
|
|
1540
|
-
return field === void 0 ? {
|
|
1541
|
-
code: "document.invalid",
|
|
1542
|
-
message,
|
|
1543
|
-
severity: "error"
|
|
1544
|
-
} : {
|
|
1545
|
-
code: "document.invalid",
|
|
1546
|
-
field,
|
|
1547
|
-
message,
|
|
1548
|
-
severity: "error"
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
//#endregion
|
|
1553
|
-
//#region ../server/src/internal/documents/document-state.ts
|
|
1554
|
-
const isDocumentCompilationFailed = Schema.is(DocumentCompilationFailed);
|
|
1555
|
-
function makeDocumentState(authored, published, snapshot) {
|
|
1556
|
-
const activeById = /* @__PURE__ */ new Map();
|
|
1557
|
-
for (const source of snapshot.active) {
|
|
1558
|
-
if (activeById.has(source.documentId)) throw new Error(`Document "${source.documentId}" appears more than once in the repository snapshot.`);
|
|
1559
|
-
activeById.set(source.documentId, source);
|
|
1560
|
-
}
|
|
1561
|
-
const authoredById = /* @__PURE__ */ new Map();
|
|
1562
|
-
for (const document of authored.documents) {
|
|
1563
|
-
if (authoredById.has(document.id)) throw new Error(`Compiled document "${document.id}" appears more than once in the authored catalog.`);
|
|
1564
|
-
if (!activeById.has(document.id)) throw new Error(`Compiled document "${document.id}" is missing from the repository snapshot.`);
|
|
1565
|
-
authoredById.set(document.id, document);
|
|
1566
|
-
}
|
|
1567
|
-
for (const documentId of activeById.keys()) if (!authoredById.has(documentId)) throw new Error(`Compiled document "${documentId}" is missing from the repository snapshot.`);
|
|
1568
|
-
return {
|
|
1569
|
-
activeById,
|
|
1570
|
-
authored,
|
|
1571
|
-
authoredById,
|
|
1572
|
-
published,
|
|
1573
|
-
snapshot
|
|
1574
|
-
};
|
|
1575
|
-
}
|
|
1576
|
-
const compileDocumentState = Effect.fn("DocumentState.compile")(function* (engine, snapshot, previous) {
|
|
1577
|
-
return makeDocumentState(yield* engine.compileAll({
|
|
1578
|
-
previous: previous?.authored,
|
|
1579
|
-
sources: authoredSources(snapshot)
|
|
1580
|
-
}), yield* engine.compileAll({
|
|
1581
|
-
previous: previous?.published,
|
|
1582
|
-
sources: publishedSources(snapshot)
|
|
1583
|
-
}), snapshot);
|
|
1584
|
-
});
|
|
1585
|
-
function authoredSources(snapshot) {
|
|
1586
|
-
return snapshot.active.map((source) => ({
|
|
1587
|
-
documentId: source.documentId,
|
|
1588
|
-
draft: source.publishedVersion !== source.version,
|
|
1589
|
-
markdown: source.markdown,
|
|
1590
|
-
sourcePath: source.sourcePath
|
|
1591
|
-
}));
|
|
1592
|
-
}
|
|
1593
|
-
function publishedSources(snapshot) {
|
|
1594
|
-
return snapshot.published.map((source) => ({
|
|
1595
|
-
documentId: source.documentId,
|
|
1596
|
-
draft: false,
|
|
1597
|
-
markdown: source.markdown,
|
|
1598
|
-
sourcePath: source.sourcePath
|
|
1599
|
-
}));
|
|
1600
|
-
}
|
|
1601
|
-
const compileValidCatalog = Effect.fn("DocumentState.compileValid")(function* (engine, sources, previous) {
|
|
1602
|
-
const result = yield* Effect.result(engine.compileAll({
|
|
1603
|
-
previous,
|
|
1604
|
-
sources
|
|
1605
|
-
}));
|
|
1606
|
-
if (Result.isFailure(result)) return yield* new DocumentValidationFailed({ issues: compilationIssues(result.failure) });
|
|
1607
|
-
const errors = result.success.issues.filter((issue) => issue.severity === "error");
|
|
1608
|
-
if (errors.length > 0) return yield* new DocumentValidationFailed({ issues: errors });
|
|
1609
|
-
return result.success;
|
|
1610
|
-
});
|
|
1611
|
-
const validateDocumentSources = Effect.fn("DocumentState.validate")(function* (engine, sources, sourcePath, previous) {
|
|
1612
|
-
const result = yield* Effect.result(engine.compileAll({
|
|
1613
|
-
previous,
|
|
1614
|
-
sources
|
|
1615
|
-
}));
|
|
1616
|
-
if (Result.isFailure(result)) return DocumentValidationResult.make({
|
|
1617
|
-
issues: compilationIssues(result.failure),
|
|
1618
|
-
valid: false
|
|
1619
|
-
});
|
|
1620
|
-
const catalog = result.success;
|
|
1621
|
-
const document = catalog.documents.find((candidate) => candidate.source.path === sourcePath);
|
|
1622
|
-
const issues = catalog.issues;
|
|
1623
|
-
const validation = {
|
|
1624
|
-
issues,
|
|
1625
|
-
valid: document !== void 0 && !issues.some((issue) => issue.severity === "error")
|
|
1626
|
-
};
|
|
1627
|
-
return DocumentValidationResult.make(Object.assign(validation, document === void 0 ? void 0 : { document }));
|
|
1628
|
-
});
|
|
1629
|
-
function authoringEntries(state, query) {
|
|
1630
|
-
return state.snapshot.active.flatMap((source) => {
|
|
1631
|
-
const document = state.authoredById.get(source.documentId);
|
|
1632
|
-
if (!document) throw new Error(`Compiled document "${source.documentId}" is missing from the repository snapshot.`);
|
|
1633
|
-
if (query.collection !== void 0 && document.collection !== query.collection) return [];
|
|
1634
|
-
if (query.locale !== void 0 && document.locale !== query.locale) return [];
|
|
1635
|
-
return [DocumentSummary.make({
|
|
1636
|
-
document,
|
|
1637
|
-
sourcePath: source.sourcePath,
|
|
1638
|
-
version: source.version
|
|
1639
|
-
})];
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
function deletedEntries(state) {
|
|
1643
|
-
return state.snapshot.deleted.map((source) => DeletedDocument.make({
|
|
1644
|
-
documentId: source.documentId,
|
|
1645
|
-
deletedAt: source.deletedAt,
|
|
1646
|
-
sourcePath: source.sourcePath,
|
|
1647
|
-
version: source.version
|
|
1648
|
-
}));
|
|
1649
|
-
}
|
|
1650
|
-
function entryById(state, documentId) {
|
|
1651
|
-
const source = state.activeById.get(documentId);
|
|
1652
|
-
const document = state.authoredById.get(documentId);
|
|
1653
|
-
return source && document ? DocumentSummary.make({
|
|
1654
|
-
document,
|
|
1655
|
-
sourcePath: source.sourcePath,
|
|
1656
|
-
version: source.version
|
|
1657
|
-
}) : void 0;
|
|
1658
|
-
}
|
|
1659
|
-
function compilationIssues(error) {
|
|
1660
|
-
if (isDocumentCompilationFailed(error)) return error.issues;
|
|
1661
|
-
return [{
|
|
1662
|
-
code: "markdown.parse",
|
|
1663
|
-
message: errorMessage(error.cause),
|
|
1664
|
-
severity: "error",
|
|
1665
|
-
sourcePath: error.sourcePath
|
|
1666
|
-
}];
|
|
1667
|
-
}
|
|
1668
|
-
function errorMessage(cause) {
|
|
1669
|
-
return cause instanceof Error && cause.message ? cause.message : "Markdown could not be parsed.";
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
//#endregion
|
|
1673
|
-
//#region ../server/src/internal/documents/document-lifecycle.ts
|
|
1674
|
-
const PreparedDocumentMutation = Data.taggedEnum();
|
|
1675
|
-
const prepareDocumentMutation = Effect.fn("DocumentLifecycle.prepare")(function* (dependencies, state, command) {
|
|
1676
|
-
const { engine, configuration, repository } = dependencies;
|
|
1677
|
-
const authored = authoredSources(state.snapshot);
|
|
1678
|
-
const published = publishedSources(state.snapshot);
|
|
1679
|
-
switch (command.action) {
|
|
1680
|
-
case "document.create": {
|
|
1681
|
-
yield* ensurePathAvailable(state, command.sourcePath);
|
|
1682
|
-
const nextAuthored = [...authored, {
|
|
1683
|
-
documentId: command.documentId,
|
|
1684
|
-
draft: true,
|
|
1685
|
-
markdown: command.markdown,
|
|
1686
|
-
sourcePath: command.sourcePath
|
|
1687
|
-
}];
|
|
1688
|
-
return PreparedDocumentMutation.Commit({
|
|
1689
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1690
|
-
published: state.published,
|
|
1691
|
-
commitPlan: {
|
|
1692
|
-
action: command.action,
|
|
1693
|
-
documentId: command.documentId,
|
|
1694
|
-
markdown: command.markdown,
|
|
1695
|
-
sourcePath: command.sourcePath
|
|
1696
|
-
}
|
|
1697
|
-
});
|
|
1698
|
-
}
|
|
1699
|
-
case "document.update": {
|
|
1700
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1701
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1702
|
-
const nextAuthored = replaceSource(authored, {
|
|
1703
|
-
documentId: current.documentId,
|
|
1704
|
-
draft: true,
|
|
1705
|
-
markdown: command.input.markdown,
|
|
1706
|
-
sourcePath: current.sourcePath
|
|
1707
|
-
});
|
|
1708
|
-
return PreparedDocumentMutation.Commit({
|
|
1709
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1710
|
-
published: state.published,
|
|
1711
|
-
commitPlan: {
|
|
1712
|
-
action: command.action,
|
|
1713
|
-
documentId: current.documentId,
|
|
1714
|
-
expectedVersion: command.input.expectedVersion,
|
|
1715
|
-
markdown: command.input.markdown,
|
|
1716
|
-
publishedVersion: current.publishedVersion,
|
|
1717
|
-
sourcePath: current.sourcePath
|
|
1718
|
-
}
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
case "document.move": {
|
|
1722
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1723
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1724
|
-
const entry = entryById(state, current.documentId);
|
|
1725
|
-
if (!entry) return yield* Effect.die(/* @__PURE__ */ new Error("Active document is missing from the authored catalog."));
|
|
1726
|
-
const sourcePath = yield* Effect.fromResult(planDocumentMove(configuration, entry.document, command.input.location));
|
|
1727
|
-
yield* ensurePathAvailable(state, sourcePath, current.documentId);
|
|
1728
|
-
const nextAuthored = replaceSource(authored, {
|
|
1729
|
-
documentId: current.documentId,
|
|
1730
|
-
draft: true,
|
|
1731
|
-
markdown: current.markdown,
|
|
1732
|
-
sourcePath
|
|
1733
|
-
});
|
|
1734
|
-
return PreparedDocumentMutation.Commit({
|
|
1735
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1736
|
-
published: state.published,
|
|
1737
|
-
commitPlan: {
|
|
1738
|
-
action: command.action,
|
|
1739
|
-
documentId: current.documentId,
|
|
1740
|
-
expectedVersion: command.input.expectedVersion,
|
|
1741
|
-
markdown: current.markdown,
|
|
1742
|
-
publishedVersion: current.publishedVersion,
|
|
1743
|
-
sourcePath
|
|
1744
|
-
}
|
|
1745
|
-
});
|
|
1746
|
-
}
|
|
1747
|
-
case "document.remove": {
|
|
1748
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1749
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1750
|
-
return PreparedDocumentMutation.Commit({
|
|
1751
|
-
authored: yield* compileValidCatalog(engine, withoutDocument(authored, current.documentId), state.authored),
|
|
1752
|
-
published: yield* compileValidCatalog(engine, withoutDocument(published, current.documentId), state.published),
|
|
1753
|
-
commitPlan: {
|
|
1754
|
-
action: command.action,
|
|
1755
|
-
documentId: current.documentId,
|
|
1756
|
-
expectedVersion: command.input.expectedVersion,
|
|
1757
|
-
markdown: current.markdown,
|
|
1758
|
-
sourcePath: current.sourcePath
|
|
1759
|
-
}
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
case "document.restore": {
|
|
1763
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1764
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1765
|
-
const historical = yield* repository.revision(current.documentId, command.input.revisionId);
|
|
1766
|
-
const nextAuthored = replaceSource(authored, {
|
|
1767
|
-
documentId: current.documentId,
|
|
1768
|
-
draft: true,
|
|
1769
|
-
markdown: historical.markdown,
|
|
1770
|
-
sourcePath: current.sourcePath
|
|
1771
|
-
});
|
|
1772
|
-
return PreparedDocumentMutation.Commit({
|
|
1773
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1774
|
-
published: state.published,
|
|
1775
|
-
commitPlan: {
|
|
1776
|
-
action: command.action,
|
|
1777
|
-
documentId: current.documentId,
|
|
1778
|
-
expectedVersion: command.input.expectedVersion,
|
|
1779
|
-
markdown: historical.markdown,
|
|
1780
|
-
publishedVersion: current.publishedVersion,
|
|
1781
|
-
revisionId: command.input.revisionId,
|
|
1782
|
-
sourcePath: current.sourcePath
|
|
1783
|
-
}
|
|
1784
|
-
});
|
|
1785
|
-
}
|
|
1786
|
-
case "document.undelete": {
|
|
1787
|
-
const deleted = yield* requireDeleted(state, command.input.documentId);
|
|
1788
|
-
yield* requireVersion(deleted, command.input.expectedVersion);
|
|
1789
|
-
yield* ensurePathAvailable(state, deleted.sourcePath);
|
|
1790
|
-
const nextAuthored = [...authored, {
|
|
1791
|
-
documentId: deleted.documentId,
|
|
1792
|
-
draft: true,
|
|
1793
|
-
markdown: deleted.markdown,
|
|
1794
|
-
sourcePath: deleted.sourcePath
|
|
1795
|
-
}];
|
|
1796
|
-
return PreparedDocumentMutation.Commit({
|
|
1797
|
-
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1798
|
-
published: state.published,
|
|
1799
|
-
commitPlan: {
|
|
1800
|
-
action: command.action,
|
|
1801
|
-
documentId: deleted.documentId,
|
|
1802
|
-
expectedVersion: command.input.expectedVersion,
|
|
1803
|
-
markdown: deleted.markdown,
|
|
1804
|
-
sourcePath: deleted.sourcePath
|
|
1805
|
-
}
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
case "document.publish": {
|
|
1809
|
-
const current = yield* requireActive(state, command.input.documentId);
|
|
1810
|
-
yield* requireVersion(current, command.input.expectedVersion);
|
|
1811
|
-
if (current.publishedVersion === current.version) {
|
|
1812
|
-
const entry = entryById(state, current.documentId);
|
|
1813
|
-
const result = {
|
|
1814
|
-
action: "document.publish",
|
|
1815
|
-
documentId: current.documentId,
|
|
1816
|
-
generation: state.snapshot.generation,
|
|
1817
|
-
revisionId: current.revisionId,
|
|
1818
|
-
sourcePath: current.sourcePath,
|
|
1819
|
-
version: current.version
|
|
1820
|
-
};
|
|
1821
|
-
return PreparedDocumentMutation.Unchanged({ result: DocumentMutationResult.make(Object.assign(result, entry === void 0 ? void 0 : { entry })) });
|
|
1822
|
-
}
|
|
1823
|
-
const source = {
|
|
1824
|
-
documentId: current.documentId,
|
|
1825
|
-
draft: false,
|
|
1826
|
-
markdown: current.markdown,
|
|
1827
|
-
sourcePath: current.sourcePath
|
|
1828
|
-
};
|
|
1829
|
-
return PreparedDocumentMutation.Commit({
|
|
1830
|
-
authored: yield* compileValidCatalog(engine, replaceSource(authored, source), state.authored),
|
|
1831
|
-
published: yield* compileValidCatalog(engine, replaceSource(published, source), state.published),
|
|
1832
|
-
commitPlan: {
|
|
1833
|
-
action: command.action,
|
|
1834
|
-
documentId: current.documentId,
|
|
1835
|
-
expectedVersion: command.input.expectedVersion,
|
|
1836
|
-
markdown: current.markdown,
|
|
1837
|
-
revisionId: current.revisionId,
|
|
1838
|
-
sourcePath: current.sourcePath
|
|
1839
|
-
}
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
});
|
|
1844
|
-
const validateDocumentUpdate = Effect.fn("DocumentLifecycle.validateUpdate")(function* (engine, state, input) {
|
|
1845
|
-
const current = yield* requireActive(state, input.documentId);
|
|
1846
|
-
yield* requireVersion(current, input.expectedVersion);
|
|
1847
|
-
const source = {
|
|
1848
|
-
documentId: current.documentId,
|
|
1849
|
-
draft: true,
|
|
1850
|
-
markdown: input.markdown,
|
|
1851
|
-
sourcePath: current.sourcePath
|
|
1852
|
-
};
|
|
1853
|
-
return yield* validateDocumentSources(engine, replaceSource(authoredSources(state.snapshot), source), current.sourcePath, state.authored);
|
|
1854
|
-
});
|
|
1855
|
-
function requireActive(state, documentId) {
|
|
1856
|
-
const source = state.activeById.get(documentId);
|
|
1857
|
-
return source ? Effect.succeed(source) : Effect.fail(new DocumentNotFound({ documentId }));
|
|
1858
|
-
}
|
|
1859
|
-
function requireDeleted(state, documentId) {
|
|
1860
|
-
const source = state.snapshot.deleted.find((candidate) => candidate.documentId === documentId);
|
|
1861
|
-
return source ? Effect.succeed(source) : Effect.fail(new DocumentNotFound({ documentId }));
|
|
1862
|
-
}
|
|
1863
|
-
function requireVersion(source, expectedVersion) {
|
|
1864
|
-
return source.version === expectedVersion ? Effect.void : Effect.fail(new DocumentVersionConflict({
|
|
1865
|
-
actualVersion: source.version,
|
|
1866
|
-
documentId: source.documentId,
|
|
1867
|
-
expectedVersion
|
|
1868
|
-
}));
|
|
1869
|
-
}
|
|
1870
|
-
function ensurePathAvailable(state, sourcePath, exceptDocumentId) {
|
|
1871
|
-
return state.snapshot.active.some((source) => source.sourcePath === sourcePath && source.documentId !== exceptDocumentId) ? Effect.fail(new DocumentPathConflict({ sourcePath })) : Effect.void;
|
|
1872
|
-
}
|
|
1873
|
-
function replaceSource(sources, source) {
|
|
1874
|
-
return [...sources.filter((candidate) => candidate.documentId !== source.documentId), source];
|
|
1875
|
-
}
|
|
1876
|
-
function withoutDocument(sources, documentId) {
|
|
1877
|
-
return sources.filter((source) => source.documentId !== documentId);
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
//#endregion
|
|
1881
|
-
//#region ../server/src/internal/documents/document-media-references.ts
|
|
1882
|
-
/** Projects the durable authored/published media-reference union without reparsing documents. */
|
|
1883
|
-
function projectMediaReferences(authored, published, documentId) {
|
|
1884
|
-
const references = [...referencesFromCatalog(authored, documentId), ...referencesFromCatalog(published, documentId)];
|
|
1885
|
-
return [...new Map(references.map((reference) => [referenceIdentity(reference), reference])).values()];
|
|
1886
|
-
}
|
|
1887
|
-
function referencesFromCatalog(catalog, documentId) {
|
|
1888
|
-
return catalog.references.all.filter((reference) => reference.target.kind === "media" && (documentId === void 0 || reference.source.documentId === documentId)).map(toMediaReference);
|
|
1889
|
-
}
|
|
1890
|
-
function toMediaReference(reference) {
|
|
1891
|
-
if (reference.target.kind !== "media") throw new Error("Expected a media reference.");
|
|
1892
|
-
return MediaReference.make({
|
|
1893
|
-
documentId: reference.source.documentId,
|
|
1894
|
-
nodePath: reference.nodePath,
|
|
1895
|
-
objectKey: MediaObjectKey.make(reference.target.key),
|
|
1896
|
-
path: reference.source.path,
|
|
1897
|
-
sourcePath: reference.source.sourcePath
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
function referenceIdentity(reference) {
|
|
1901
|
-
return `${reference.documentId}:${reference.sourcePath}:${reference.nodePath.join(".")}:${reference.objectKey}`;
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
//#endregion
|
|
1905
|
-
//#region ../server/src/internal/documents/document-import.ts
|
|
1906
|
-
/** Imports configured or migrated Sources through the normal Document lifecycle. */
|
|
1907
|
-
const importDocumentSources = Effect.fn("Documents.importSources")(function* (dependencies, input, activity) {
|
|
1908
|
-
const report = {
|
|
1909
|
-
created: 0,
|
|
1910
|
-
preserved: 0,
|
|
1911
|
-
published: 0,
|
|
1912
|
-
unchanged: 0,
|
|
1913
|
-
updated: 0
|
|
1914
|
-
};
|
|
1915
|
-
for (const source of input.sources) {
|
|
1916
|
-
const plan = planDocumentImport(yield* dependencies.current(source.sourcePath), source, input.mode);
|
|
1917
|
-
switch (plan.kind) {
|
|
1918
|
-
case "create": {
|
|
1919
|
-
const created = yield* dependencies.runMutation({
|
|
1920
|
-
action: "document.create",
|
|
1921
|
-
documentId: yield* dependencies.nextDocumentId,
|
|
1922
|
-
markdown: source.markdown,
|
|
1923
|
-
sourcePath: source.sourcePath
|
|
1924
|
-
}, activity, input.reason);
|
|
1925
|
-
report.created += 1;
|
|
1926
|
-
if (plan.publish) {
|
|
1927
|
-
yield* publish(dependencies, created, input, activity);
|
|
1928
|
-
report.published += 1;
|
|
1929
|
-
}
|
|
1930
|
-
break;
|
|
1931
|
-
}
|
|
1932
|
-
case "preserve":
|
|
1933
|
-
case "unchanged":
|
|
1934
|
-
report[plan.kind === "preserve" ? "preserved" : "unchanged"] += 1;
|
|
1935
|
-
break;
|
|
1936
|
-
case "publish":
|
|
1937
|
-
yield* publish(dependencies, plan.current, input, activity);
|
|
1938
|
-
report.published += 1;
|
|
1939
|
-
break;
|
|
1940
|
-
case "update": {
|
|
1941
|
-
const updated = yield* dependencies.runMutation({
|
|
1942
|
-
action: "document.update",
|
|
1943
|
-
input: {
|
|
1944
|
-
documentId: plan.current.documentId,
|
|
1945
|
-
expectedVersion: plan.current.version,
|
|
1946
|
-
markdown: source.markdown,
|
|
1947
|
-
reason: input.reason
|
|
1948
|
-
}
|
|
1949
|
-
}, activity, input.reason);
|
|
1950
|
-
report.updated += 1;
|
|
1951
|
-
if (plan.publish) {
|
|
1952
|
-
yield* publish(dependencies, updated, input, activity);
|
|
1953
|
-
report.published += 1;
|
|
1954
|
-
}
|
|
1955
|
-
break;
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
return DocumentImportReport.make(report);
|
|
1960
|
-
});
|
|
1961
|
-
/** Plans one deterministic import without performing persistence or generating identities. */
|
|
1962
|
-
function planDocumentImport(current, source, mode) {
|
|
1963
|
-
if (!current) return {
|
|
1964
|
-
kind: "create",
|
|
1965
|
-
publish: source.published
|
|
1966
|
-
};
|
|
1967
|
-
if (current.markdown !== source.markdown) return mode === "overwrite" ? {
|
|
1968
|
-
current,
|
|
1969
|
-
kind: "update",
|
|
1970
|
-
publish: source.published
|
|
1971
|
-
} : { kind: "preserve" };
|
|
1972
|
-
if (source.published && current.publishedVersion !== current.version) return {
|
|
1973
|
-
current,
|
|
1974
|
-
kind: "publish"
|
|
1975
|
-
};
|
|
1976
|
-
return { kind: "unchanged" };
|
|
1977
|
-
}
|
|
1978
|
-
function publish(dependencies, source, input, activity) {
|
|
1979
|
-
return dependencies.runMutation({
|
|
1980
|
-
action: "document.publish",
|
|
1981
|
-
input: {
|
|
1982
|
-
documentId: source.documentId,
|
|
1983
|
-
expectedVersion: source.version,
|
|
1984
|
-
reason: input.reason
|
|
1985
|
-
}
|
|
1986
|
-
}, activity, input.reason);
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
//#endregion
|
|
1990
|
-
//#region ../server/src/internal/documents/document-store.ts
|
|
1991
|
-
var DocumentStore = class extends Context.Service()("@internal/server/DocumentStore") {};
|
|
1992
|
-
|
|
1993
|
-
//#endregion
|
|
1994
|
-
//#region ../server/src/internal/documents/document-failures.ts
|
|
1995
|
-
var DocumentPersistenceError = class extends Schema.TaggedError()("Documents.PersistenceError", {
|
|
1996
|
-
operation: Schema.NonEmptyString,
|
|
1997
|
-
cause: Schema.Defect()
|
|
1998
|
-
}) {};
|
|
1999
|
-
var DocumentGenerationConflict = class extends Schema.TaggedError()("Documents.GenerationConflict", {
|
|
2000
|
-
expectedGeneration: DocumentGeneration,
|
|
2001
|
-
actualGeneration: DocumentGeneration
|
|
2002
|
-
}) {};
|
|
2003
|
-
|
|
2004
|
-
//#endregion
|
|
2005
|
-
//#region ../server/src/internal/documents/document-history.ts
|
|
2006
|
-
const CursorPayload$1 = Schema.Struct({
|
|
2007
|
-
createdAt: Schema.DateTimeUtcFromString,
|
|
2008
|
-
revisionId: RevisionId
|
|
2009
|
-
});
|
|
2010
|
-
const CursorJson$1 = Schema.fromJsonString(CursorPayload$1);
|
|
2011
|
-
const cursors$1 = cursorCodec(CursorJson$1, (value) => DocumentHistoryCursor.make(value), () => new DocumentHistoryInvalidCursor());
|
|
2012
|
-
function documentHistoryCapability(accessControl, repository) {
|
|
2013
|
-
const requireHistory = (context, documentId) => accessControl.require(context, {
|
|
2014
|
-
action: "document.history",
|
|
2015
|
-
resource: {
|
|
2016
|
-
documentId,
|
|
2017
|
-
kind: "document"
|
|
2018
|
-
}
|
|
2019
|
-
});
|
|
2020
|
-
const read = Effect.fn("Documents.history.read")(function* (context, documentId, revisionId) {
|
|
2021
|
-
yield* requireHistory(context, documentId);
|
|
2022
|
-
return yield* repository.revision(documentId, revisionId);
|
|
2023
|
-
});
|
|
2024
|
-
const list = Effect.fn("Documents.history.list")(function* (context, query) {
|
|
2025
|
-
yield* requireHistory(context, query.documentId);
|
|
2026
|
-
const cursor = query.cursor ? yield* Effect.fromResult(cursors$1.decode(query.cursor)) : void 0;
|
|
2027
|
-
const page = yield* repository.revisions({
|
|
2028
|
-
cursor,
|
|
2029
|
-
documentId: query.documentId,
|
|
2030
|
-
limit: query.limit ?? 25
|
|
2031
|
-
});
|
|
2032
|
-
const output = {
|
|
2033
|
-
revisions: page.revisions.map(({ markdown: _markdown, ...revision }) => revision),
|
|
2034
|
-
total: page.total
|
|
2035
|
-
};
|
|
2036
|
-
return page.next ? DocumentRevisionPage.make({
|
|
2037
|
-
...output,
|
|
2038
|
-
nextCursor: cursors$1.encode(page.next)
|
|
2039
|
-
}) : DocumentRevisionPage.make(output);
|
|
2040
|
-
});
|
|
2041
|
-
return {
|
|
2042
|
-
compare: Effect.fn("Documents.history.compare")(function* (context, input) {
|
|
2043
|
-
yield* requireHistory(context, input.documentId);
|
|
2044
|
-
const from = yield* repository.revision(input.documentId, input.fromRevisionId);
|
|
2045
|
-
const to = yield* repository.revision(input.documentId, input.toRevisionId);
|
|
2046
|
-
const changes = yield* compareLines(from.markdown, to.markdown);
|
|
2047
|
-
return DocumentRevisionComparison.make({
|
|
2048
|
-
changes,
|
|
2049
|
-
from,
|
|
2050
|
-
to
|
|
2051
|
-
});
|
|
2052
|
-
}),
|
|
2053
|
-
list,
|
|
2054
|
-
read
|
|
2055
|
-
};
|
|
2056
|
-
}
|
|
2057
|
-
const MAXIMUM_COMPARISON_LINES = 1e4;
|
|
2058
|
-
const MAXIMUM_EDIT_LENGTH = 2e3;
|
|
2059
|
-
const comparisonTooLarge = () => new DocumentHistoryComparisonTooLarge({
|
|
2060
|
-
maximumEditLength: MAXIMUM_EDIT_LENGTH,
|
|
2061
|
-
maximumLines: MAXIMUM_COMPARISON_LINES
|
|
2062
|
-
});
|
|
2063
|
-
const compareLines = Effect.fn("Documents.history.compareLines")(function* (beforeMarkdown, afterMarkdown) {
|
|
2064
|
-
const before = beforeMarkdown.split("\n");
|
|
2065
|
-
const after = afterMarkdown.split("\n");
|
|
2066
|
-
if (before.length > MAXIMUM_COMPARISON_LINES || after.length > MAXIMUM_COMPARISON_LINES) return yield* comparisonTooLarge();
|
|
2067
|
-
const diff = diffArrays(before, after, {
|
|
2068
|
-
maxEditLength: MAXIMUM_EDIT_LENGTH,
|
|
2069
|
-
oneChangePerToken: true,
|
|
2070
|
-
timeout: 100
|
|
2071
|
-
});
|
|
2072
|
-
if (!diff) return yield* comparisonTooLarge();
|
|
2073
|
-
const changes = [];
|
|
2074
|
-
let beforeLine = 1;
|
|
2075
|
-
let afterLine = 1;
|
|
2076
|
-
for (const change of diff) for (const line of change.value) if (change.removed) {
|
|
2077
|
-
changes.push({
|
|
2078
|
-
beforeLine,
|
|
2079
|
-
kind: "removed",
|
|
2080
|
-
line
|
|
2081
|
-
});
|
|
2082
|
-
beforeLine += 1;
|
|
2083
|
-
} else if (change.added) {
|
|
2084
|
-
changes.push({
|
|
2085
|
-
afterLine,
|
|
2086
|
-
kind: "added",
|
|
2087
|
-
line
|
|
2088
|
-
});
|
|
2089
|
-
afterLine += 1;
|
|
2090
|
-
} else {
|
|
2091
|
-
changes.push({
|
|
2092
|
-
afterLine,
|
|
2093
|
-
beforeLine,
|
|
2094
|
-
kind: "equal",
|
|
2095
|
-
line
|
|
2096
|
-
});
|
|
2097
|
-
beforeLine += 1;
|
|
2098
|
-
afterLine += 1;
|
|
2099
|
-
}
|
|
2100
|
-
return changes;
|
|
2101
|
-
});
|
|
2102
|
-
|
|
2103
|
-
//#endregion
|
|
2104
|
-
//#region ../server/src/internal/documents/published-documents.ts
|
|
2105
|
-
function publishedDocuments(configuration, catalog) {
|
|
2106
|
-
const read = Effect.fn("Documents.published.read")(function* (lookup) {
|
|
2107
|
-
const document = findPublishedDocument(yield* catalog, lookup);
|
|
2108
|
-
return Option.isSome(document) ? document.value : yield* new PublishedDocumentNotFound({ lookup });
|
|
2109
|
-
});
|
|
2110
|
-
const query = Effect.fn("Documents.published.query")(function* (input) {
|
|
2111
|
-
return queryDocuments((yield* catalog).documents, input, {
|
|
2112
|
-
limit: input.limit,
|
|
2113
|
-
offset: input.offset ?? 0
|
|
2114
|
-
});
|
|
2115
|
-
});
|
|
2116
|
-
const navigation = Effect.fn("Documents.published.navigation")(function* (input) {
|
|
2117
|
-
return createNavigation((yield* catalog).documents, input, configuration);
|
|
2118
|
-
});
|
|
2119
|
-
const surround = Effect.fn("Documents.published.surround")(function* (lookup, input) {
|
|
2120
|
-
const current = yield* catalog;
|
|
2121
|
-
const document = findPublishedDocument(current, lookup);
|
|
2122
|
-
if (Option.isNone(document)) return yield* new PublishedDocumentNotFound({ lookup });
|
|
2123
|
-
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 });
|
|
2124
|
-
const page = queryDocuments(current.documents, surroundQuery);
|
|
2125
|
-
const index = page.items.findIndex((candidate) => candidate.id === document.value.id);
|
|
2126
|
-
return DocumentSurround.make({
|
|
2127
|
-
previous: index > 0 ? page.items[index - 1] ?? null : null,
|
|
2128
|
-
next: index >= 0 ? page.items[index + 1] ?? null : null
|
|
2129
|
-
});
|
|
2130
|
-
});
|
|
2131
|
-
return {
|
|
2132
|
-
diagnostics: catalog.pipe(Effect.map((current) => current.issues)),
|
|
2133
|
-
navigation,
|
|
2134
|
-
query,
|
|
2135
|
-
read,
|
|
2136
|
-
surround
|
|
2137
|
-
};
|
|
2138
|
-
}
|
|
2139
|
-
function findPublishedDocument(catalog, lookup) {
|
|
2140
|
-
switch (lookup.kind) {
|
|
2141
|
-
case "id": return Option.fromNullishOr(catalog.documents.find((document) => document.id === lookup.documentId));
|
|
2142
|
-
case "path": return catalog.findByPath(lookup.path);
|
|
2143
|
-
case "key": return catalog.findByKey(lookup.key, lookup.locale).pipe(Option.filter((document) => lookup.collection === void 0 || document.collection === lookup.collection));
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
function queryDocuments(documents, query, page) {
|
|
2147
|
-
const filtered = documents.filter((document) => {
|
|
2148
|
-
if (query.collection !== void 0 && document.collection !== query.collection) return false;
|
|
2149
|
-
if (query.locale !== void 0 && document.locale !== query.locale) return false;
|
|
2150
|
-
return Object.entries(query.where ?? {}).every(([field, expected]) => matchesValue(documentField(document, field), expected));
|
|
2151
|
-
});
|
|
2152
|
-
const sorted = query.sort && query.sort.length > 0 ? sortDocuments(filtered, query.sort) : filtered;
|
|
2153
|
-
const items = page ? sorted.slice(page.offset, page.offset + page.limit) : sorted;
|
|
2154
|
-
return PublishedDocumentPage.make({
|
|
2155
|
-
items,
|
|
2156
|
-
total: filtered.length
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
function createNavigation(documents, query, configuration) {
|
|
2160
|
-
const locale = query.locale ?? configuration.locales?.defaultLocale;
|
|
2161
|
-
const candidates = documents.filter((document) => (query.collection === void 0 || document.collection === query.collection) && (locale === void 0 || document.locale === locale) && document.data.navigation !== false);
|
|
2162
|
-
const roots = [];
|
|
2163
|
-
const itemsByKey = /* @__PURE__ */ new Map();
|
|
2164
|
-
const orderByItem = /* @__PURE__ */ new Map();
|
|
2165
|
-
for (const document of candidates) {
|
|
2166
|
-
if (document.key === "/") {
|
|
2167
|
-
const item = navigationItem(document);
|
|
2168
|
-
roots.push(item);
|
|
2169
|
-
orderByItem.set(item, Number.NEGATIVE_INFINITY);
|
|
2170
|
-
continue;
|
|
2171
|
-
}
|
|
2172
|
-
const segments = document.key.slice(1).split("/");
|
|
2173
|
-
let parent;
|
|
2174
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
2175
|
-
const key = PublicPath.make(`/${segments.slice(0, index + 1).join("/")}`);
|
|
2176
|
-
let item = itemsByKey.get(key);
|
|
2177
|
-
if (!item) {
|
|
2178
|
-
item = {
|
|
2179
|
-
children: [],
|
|
2180
|
-
key,
|
|
2181
|
-
title: titleFromSegment(segments[index] ?? "")
|
|
1240
|
+
const user = yield* decodeUser(result.user, "BetterAuth.recovery.findUserByEmail");
|
|
1241
|
+
return {
|
|
1242
|
+
creationChangeId: yield* decodeBetterAuthUserCreationChangeId(result.user).pipe(Effect.mapError((cause) => unavailable$4("BetterAuth.recovery.creationChangeId", cause))),
|
|
1243
|
+
credentialReady: hasCredential(result.accounts),
|
|
1244
|
+
kind: "create",
|
|
1245
|
+
user
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
case "remove": {
|
|
1249
|
+
const user = yield* findUser(adapter, intent.userId);
|
|
1250
|
+
return {
|
|
1251
|
+
kind: "remove",
|
|
1252
|
+
sessionFingerprints: yield* findSessionFingerprints(adapter, intent.userId),
|
|
1253
|
+
user
|
|
2182
1254
|
};
|
|
2183
|
-
itemsByKey.set(key, item);
|
|
2184
|
-
if (parent) parent.children.push(item);
|
|
2185
|
-
else roots.push(item);
|
|
2186
1255
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
1256
|
+
case "change-role": return {
|
|
1257
|
+
kind: "change-role",
|
|
1258
|
+
user: yield* findUser(adapter, intent.userId)
|
|
1259
|
+
};
|
|
1260
|
+
case "revoke-sessions": {
|
|
1261
|
+
const rawUser = yield* findRawUser(adapter, intent.userId);
|
|
1262
|
+
const user = rawUser ? yield* decodeUser(rawUser, "BetterAuth.recovery.findUserById") : null;
|
|
1263
|
+
return {
|
|
1264
|
+
kind: "revoke-sessions",
|
|
1265
|
+
sessionFingerprints: yield* findSessionFingerprints(adapter, intent.userId),
|
|
1266
|
+
sessionsRevokedChangeId: rawUser ? yield* decodeBetterAuthUserSessionsRevokedChangeId(rawUser).pipe(Effect.mapError((cause) => unavailable$4("BetterAuth.recovery.sessionsRevokedChangeId", cause))) : null,
|
|
1267
|
+
user
|
|
1268
|
+
};
|
|
2194
1269
|
}
|
|
2195
|
-
parent = item;
|
|
2196
1270
|
}
|
|
2197
|
-
}
|
|
2198
|
-
sortNavigation(roots, orderByItem);
|
|
2199
|
-
return roots.map((item) => DocumentNavigationItem.make(item));
|
|
2200
|
-
}
|
|
2201
|
-
function navigationItem(document) {
|
|
2202
|
-
const navigation = document.data.navigation;
|
|
2203
|
-
const configuredTitle = Schema.is(Schema.JsonObject)(navigation) ? navigation.title : void 0;
|
|
1271
|
+
}) });
|
|
2204
1272
|
return {
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
1273
|
+
initial: InitialIdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeInitialAdministrator")(function* (intent) {
|
|
1274
|
+
const adapter = yield* initialAdapter;
|
|
1275
|
+
const result = yield* providerCall("BetterAuth.recovery.findInitialAdministrator", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), initializationUnavailable$1);
|
|
1276
|
+
if (!result) return {
|
|
1277
|
+
credentialReady: false,
|
|
1278
|
+
user: null
|
|
1279
|
+
};
|
|
1280
|
+
return {
|
|
1281
|
+
credentialReady: hasCredential(result.accounts),
|
|
1282
|
+
user: yield* decodeBetterAuthUser(result.user).pipe(Effect.mapError((cause) => initializationUnavailable$1("BetterAuth.recovery.findInitialAdministrator", cause)))
|
|
1283
|
+
};
|
|
1284
|
+
}) }),
|
|
1285
|
+
users
|
|
2211
1286
|
};
|
|
2212
1287
|
}
|
|
2213
|
-
function
|
|
2214
|
-
|
|
2215
|
-
const order = (Schema.is(Schema.JsonObject)(navigation) ? navigation.order : void 0) ?? document.data.order;
|
|
2216
|
-
return Schema.is(Schema.Finite)(order) ? order : Number.POSITIVE_INFINITY;
|
|
2217
|
-
}
|
|
2218
|
-
function sortNavigation(items, orderByItem) {
|
|
2219
|
-
items.sort((left, right) => {
|
|
2220
|
-
return (orderByItem.get(left) ?? Number.POSITIVE_INFINITY) - (orderByItem.get(right) ?? Number.POSITIVE_INFINITY) || left.title.localeCompare(right.title);
|
|
2221
|
-
});
|
|
2222
|
-
items.forEach((item) => sortNavigation(item.children, orderByItem));
|
|
2223
|
-
}
|
|
2224
|
-
function documentField(document, field) {
|
|
2225
|
-
switch (field) {
|
|
2226
|
-
case "collection": return document.collection;
|
|
2227
|
-
case "description": return document.description;
|
|
2228
|
-
case "id": return document.id;
|
|
2229
|
-
case "key": return document.key;
|
|
2230
|
-
case "locale": return document.locale;
|
|
2231
|
-
case "path": return document.path;
|
|
2232
|
-
case "source.path": return document.source.path;
|
|
2233
|
-
case "title": return document.title;
|
|
2234
|
-
default: return nestedValue(document.data, field.startsWith("data.") ? field.slice(5) : field);
|
|
2235
|
-
}
|
|
1288
|
+
function hasCredential(accounts) {
|
|
1289
|
+
return accounts.some((account) => account.providerId === "credential" && (account.password?.length ?? 0) > 0);
|
|
2236
1290
|
}
|
|
2237
|
-
function
|
|
2238
|
-
return
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
}
|
|
2243
|
-
function matchesValue(actual, expected) {
|
|
2244
|
-
const expectedValues = Array.isArray(expected) ? expected : [expected];
|
|
2245
|
-
return (Array.isArray(actual) ? actual : [actual]).some((value) => Schema.is(DocumentQueryScalar)(value) && expectedValues.includes(value));
|
|
2246
|
-
}
|
|
2247
|
-
function sortDocuments(documents, sorts) {
|
|
2248
|
-
return documents.toSorted((left, right) => {
|
|
2249
|
-
for (const sort of sorts) {
|
|
2250
|
-
const comparison = compareValues(documentField(left, sort.field), documentField(right, sort.field));
|
|
2251
|
-
if (comparison !== 0) return sort.direction === "desc" ? -comparison : comparison;
|
|
2252
|
-
}
|
|
2253
|
-
return 0;
|
|
2254
|
-
});
|
|
1291
|
+
function decodeUser(user, operation) {
|
|
1292
|
+
return decodeBetterAuthUser(user).pipe(Effect.mapError((cause) => unavailable$4(operation, cause)));
|
|
1293
|
+
}
|
|
1294
|
+
function findUser(adapter, userId) {
|
|
1295
|
+
return findRawUser(adapter, userId).pipe(Effect.flatMap((user) => user ? decodeUser(user, "BetterAuth.recovery.findUserById") : Effect.succeed(null)));
|
|
2255
1296
|
}
|
|
2256
|
-
function
|
|
2257
|
-
|
|
2258
|
-
if (left === void 0 || left === null) return 1;
|
|
2259
|
-
if (right === void 0 || right === null) return -1;
|
|
2260
|
-
if (Schema.is(Schema.Finite)(left) && Schema.is(Schema.Finite)(right)) return left - right;
|
|
2261
|
-
const leftText = Schema.is(Schema.String)(left) ? left : JSON.stringify(left) ?? "";
|
|
2262
|
-
const rightText = Schema.is(Schema.String)(right) ? right : JSON.stringify(right) ?? "";
|
|
2263
|
-
return leftText.localeCompare(rightText);
|
|
1297
|
+
function findRawUser(adapter, userId) {
|
|
1298
|
+
return providerCall("BetterAuth.recovery.findUserById", () => adapter.findUserById(userId), unavailable$4);
|
|
2264
1299
|
}
|
|
2265
|
-
function
|
|
2266
|
-
return
|
|
1300
|
+
function findSessionFingerprints(adapter, userId) {
|
|
1301
|
+
return providerCall("BetterAuth.recovery.listSessions", () => adapter.listSessions(userId), unavailable$4).pipe(Effect.map((sessions) => sessions.map((session) => sessionFingerprint(session.id))));
|
|
2267
1302
|
}
|
|
2268
1303
|
|
|
2269
1304
|
//#endregion
|
|
2270
|
-
//#region ../
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
const semaphore = yield* Semaphore.make(1);
|
|
2285
|
-
const mutationEvents = yield* Effect.acquireRelease(PubSub.sliding(CONTENT_MUTATION_BUFFER_CAPACITY), PubSub.shutdown);
|
|
2286
|
-
const dependencies = {
|
|
2287
|
-
engine,
|
|
2288
|
-
configuration,
|
|
2289
|
-
repository
|
|
2290
|
-
};
|
|
2291
|
-
const refreshUnlocked = Effect.fn("Documents.refreshUnlocked")(function* (force = false) {
|
|
2292
|
-
const current = yield* Ref.get(state);
|
|
2293
|
-
const snapshot = force ? yield* loadRequired(repository) : yield* repository.load(current.snapshot.generation);
|
|
2294
|
-
if (!snapshot || !generationIsAfter(snapshot.generation, current.snapshot.generation)) return current.snapshot.generation;
|
|
2295
|
-
const refreshed = yield* compileDocumentState(engine, snapshot, current);
|
|
2296
|
-
yield* Ref.set(state, refreshed);
|
|
2297
|
-
return refreshed.snapshot.generation;
|
|
2298
|
-
});
|
|
2299
|
-
const executeUnlocked = Effect.fn("Documents.execute")(function* (command, activity) {
|
|
2300
|
-
return yield* Effect.gen(function* () {
|
|
2301
|
-
yield* refreshUnlocked();
|
|
2302
|
-
const current = yield* Ref.get(state);
|
|
2303
|
-
const prepared = yield* prepareDocumentMutation(dependencies, current, command);
|
|
2304
|
-
if (prepared._tag === "Unchanged") return prepared.result;
|
|
2305
|
-
const committed = yield* repository.commit({
|
|
2306
|
-
activity,
|
|
2307
|
-
plan: prepared.commitPlan,
|
|
2308
|
-
expectedGeneration: current.snapshot.generation,
|
|
2309
|
-
mediaReferences: projectMediaReferences(prepared.authored, prepared.published, commandDocumentId(command))
|
|
2310
|
-
});
|
|
2311
|
-
const next = makeDocumentState(prepared.authored, prepared.published, committed.snapshot);
|
|
2312
|
-
yield* Ref.set(state, next);
|
|
2313
|
-
const entry = entryById(next, committed.mutation.documentId);
|
|
2314
|
-
const result = DocumentMutationResult.make(Object.assign({}, committed.mutation, entry === void 0 ? void 0 : { entry }));
|
|
2315
|
-
yield* PubSub.publish(mutationEvents, result);
|
|
2316
|
-
return result;
|
|
2317
|
-
}).pipe(Effect.tapError((error) => isDocumentGenerationConflict$2(error) ? refreshUnlocked(true) : Effect.void), Effect.retry({
|
|
2318
|
-
schedule: Schedule.recurs(3),
|
|
2319
|
-
while: isDocumentGenerationConflict$2
|
|
2320
|
-
}));
|
|
2321
|
-
});
|
|
2322
|
-
const runMutation = Effect.fn("Documents.runMutation")(function* (command, activity, reason) {
|
|
2323
|
-
const resolvedActivity = enrichActivity(activity, reason);
|
|
2324
|
-
return yield* semaphore.withPermits(1)(executeUnlocked(command, resolvedActivity));
|
|
2325
|
-
});
|
|
2326
|
-
const authored = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2327
|
-
yield* refreshUnlocked();
|
|
2328
|
-
return (yield* Ref.get(state)).authored;
|
|
2329
|
-
})).pipe(Effect.withSpan("Documents.authored"));
|
|
2330
|
-
const browse = Effect.fn("Documents.browse")((query) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2331
|
-
yield* refreshUnlocked();
|
|
2332
|
-
return authoringEntries(yield* Ref.get(state), query);
|
|
2333
|
-
})));
|
|
2334
|
-
const create = Effect.fn("Documents.create")(function* (input, activity) {
|
|
2335
|
-
const plan = yield* Effect.fromResult(planDocumentCreation(configuration, input));
|
|
2336
|
-
const source = yield* createInitialDocumentSource(plan);
|
|
2337
|
-
const documentId = DocumentId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
2338
|
-
return yield* runMutation({
|
|
2339
|
-
action: "document.create",
|
|
2340
|
-
documentId,
|
|
2341
|
-
markdown: source.markdown,
|
|
2342
|
-
sourcePath: plan.sourcePath
|
|
2343
|
-
}, activity, input.reason);
|
|
2344
|
-
});
|
|
2345
|
-
const generation = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2346
|
-
yield* refreshUnlocked();
|
|
2347
|
-
return (yield* Ref.get(state)).snapshot.generation;
|
|
1305
|
+
//#region ../auth/src/BetterAuth.ts
|
|
1306
|
+
function betterAuthLayer(options) {
|
|
1307
|
+
if (options.basePath !== void 0 && options.basePath !== "/api/cms/auth") throw new Error(`Authentication basePath must be ${CMS_AUTHENTICATION_PATH}.`);
|
|
1308
|
+
return Layer.effectContext(Effect.gen(function* () {
|
|
1309
|
+
const crypto = yield* Crypto.Crypto;
|
|
1310
|
+
const storage = yield* ActivityStore;
|
|
1311
|
+
const runPromise = Effect.runPromiseWith(yield* Effect.context());
|
|
1312
|
+
const auth = createCmsBetterAuth(options, betterAuthActivityOptions(betterAuthActivityWriter(storage, crypto), crypto, runPromise));
|
|
1313
|
+
const identity = betterAuthIdentity(auth);
|
|
1314
|
+
const recovery = betterAuthRecovery(auth);
|
|
1315
|
+
const initialization = betterAuthInitialIdentity(auth);
|
|
1316
|
+
const basePath = options.basePath ?? "/api/cms/auth";
|
|
1317
|
+
const handler = AuthenticationHandler.of({ handle: rejectExternalAdminRoutes(auth.handler, basePath) });
|
|
1318
|
+
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));
|
|
2348
1319
|
}));
|
|
2349
|
-
const importSources = (input, activity) => importDocumentSources({
|
|
2350
|
-
current: (sourcePath) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2351
|
-
yield* refreshUnlocked();
|
|
2352
|
-
return (yield* Ref.get(state)).snapshot.active.find((candidate) => candidate.sourcePath === sourcePath);
|
|
2353
|
-
})),
|
|
2354
|
-
nextDocumentId: crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((value) => DocumentId.make(value))),
|
|
2355
|
-
runMutation
|
|
2356
|
-
}, input, activity);
|
|
2357
|
-
const move = Effect.fn("Documents.move")((input, activity) => runMutation({
|
|
2358
|
-
action: "document.move",
|
|
2359
|
-
input
|
|
2360
|
-
}, activity, input.reason));
|
|
2361
|
-
const subscribeMutations = Effect.gen(function* () {
|
|
2362
|
-
const subscription = yield* PubSub.subscribe(mutationEvents);
|
|
2363
|
-
const baseline = yield* semaphore.withPermits(1)(Ref.get(state).pipe(Effect.map((current) => current.snapshot.generation)));
|
|
2364
|
-
return {
|
|
2365
|
-
baseline,
|
|
2366
|
-
mutations: Stream.fromSubscription(subscription).pipe(Stream.filter((mutation) => generationIsAfter(mutation.generation, baseline)))
|
|
2367
|
-
};
|
|
2368
|
-
}).pipe(Effect.withSpan("Documents.subscribeMutations"));
|
|
2369
|
-
const publish = Effect.fn("Documents.publish")((input, activity) => runMutation({
|
|
2370
|
-
action: "document.publish",
|
|
2371
|
-
input
|
|
2372
|
-
}, activity, input.reason));
|
|
2373
|
-
const published = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2374
|
-
yield* refreshUnlocked();
|
|
2375
|
-
return (yield* Ref.get(state)).published;
|
|
2376
|
-
})).pipe(Effect.withSpan("Documents.published"));
|
|
2377
|
-
const read = Effect.fn("Documents.read")((documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2378
|
-
yield* refreshUnlocked();
|
|
2379
|
-
return (yield* Ref.get(state)).activeById.get(documentId) || (yield* new DocumentNotFound({ documentId }));
|
|
2380
|
-
})));
|
|
2381
|
-
const refresh = semaphore.withPermits(1)(refreshUnlocked()).pipe(Effect.withSpan("Documents.refresh"));
|
|
2382
|
-
const remove = Effect.fn("Documents.remove")((input, activity) => runMutation({
|
|
2383
|
-
action: "document.remove",
|
|
2384
|
-
input
|
|
2385
|
-
}, activity, input.reason));
|
|
2386
|
-
const restore = Effect.fn("Documents.restore")((input, activity) => runMutation({
|
|
2387
|
-
action: "document.restore",
|
|
2388
|
-
input
|
|
2389
|
-
}, activity, input.reason));
|
|
2390
|
-
const trash = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2391
|
-
yield* refreshUnlocked();
|
|
2392
|
-
return deletedEntries(yield* Ref.get(state));
|
|
2393
|
-
})).pipe(Effect.withSpan("Documents.trash"));
|
|
2394
|
-
const undelete = Effect.fn("Documents.undelete")((input, activity) => runMutation({
|
|
2395
|
-
action: "document.undelete",
|
|
2396
|
-
input
|
|
2397
|
-
}, activity, input.reason));
|
|
2398
|
-
const update = Effect.fn("Documents.update")((input, activity) => runMutation({
|
|
2399
|
-
action: "document.update",
|
|
2400
|
-
input
|
|
2401
|
-
}, activity, input.reason));
|
|
2402
|
-
const validate = Effect.fn("Documents.validate")((input) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2403
|
-
yield* refreshUnlocked();
|
|
2404
|
-
return yield* validateDocumentUpdate(engine, yield* Ref.get(state), input);
|
|
2405
|
-
})));
|
|
2406
|
-
const history = documentHistoryCapability(accessControl, repository);
|
|
2407
|
-
const delivery = publishedDocuments(configuration, published);
|
|
2408
|
-
const document = (documentId) => ({
|
|
2409
|
-
documentId,
|
|
2410
|
-
kind: "document"
|
|
2411
|
-
});
|
|
2412
|
-
const requireThen = (context, action, resource, effect) => accessControl.require(context, {
|
|
2413
|
-
action,
|
|
2414
|
-
resource
|
|
2415
|
-
}).pipe(Effect.andThen(effect));
|
|
2416
|
-
const documents = Documents.of({
|
|
2417
|
-
browse: (context, query) => requireThen(context, "document.browse", query.collection ? {
|
|
2418
|
-
collection: query.collection,
|
|
2419
|
-
kind: "collection",
|
|
2420
|
-
locale: query.locale
|
|
2421
|
-
} : { kind: "catalog" }, () => browse(query)),
|
|
2422
|
-
create: (context, input) => requireThen(context, "document.create", {
|
|
2423
|
-
collection: input.collection,
|
|
2424
|
-
kind: "collection",
|
|
2425
|
-
locale: input.locale
|
|
2426
|
-
}, () => create(input, requestActivity(context, input.reason))),
|
|
2427
|
-
historyCompare: history.compare,
|
|
2428
|
-
historyList: history.list,
|
|
2429
|
-
historyRead: history.read,
|
|
2430
|
-
move: (context, input) => requireThen(context, "document.move", document(input.documentId), () => move(input, requestActivity(context, input.reason))),
|
|
2431
|
-
publish: (context, input) => requireThen(context, "document.publish", document(input.documentId), () => publish(input, requestActivity(context, input.reason))),
|
|
2432
|
-
read: (context, documentId) => requireThen(context, "document.read", document(documentId), () => read(documentId)),
|
|
2433
|
-
remove: (context, input) => requireThen(context, "document.remove", document(input.documentId), () => remove(input, requestActivity(context, input.reason))),
|
|
2434
|
-
restore: (context, input) => requireThen(context, "document.restore", document(input.documentId), () => restore(input, requestActivity(context, input.reason))),
|
|
2435
|
-
trash: (context) => requireThen(context, "document.trash", { kind: "catalog" }, () => trash),
|
|
2436
|
-
undelete: (context, input) => requireThen(context, "document.undelete", document(input.documentId), () => undelete(input, requestActivity(context, input.reason))),
|
|
2437
|
-
update: (context, input) => requireThen(context, "document.update", document(input.documentId), () => update(input, requestActivity(context, input.reason))),
|
|
2438
|
-
validate: (context, input) => requireThen(context, "document.validate", document(input.documentId), () => validate(input))
|
|
2439
|
-
});
|
|
2440
|
-
const previews = DocumentPreviews.of({
|
|
2441
|
-
generation,
|
|
2442
|
-
read: (lookup) => authored.pipe(Effect.map((catalog) => findPublishedDocument(catalog, lookup)))
|
|
2443
|
-
});
|
|
2444
|
-
const publishedService = PublishedDocuments.of({
|
|
2445
|
-
navigation: delivery.navigation,
|
|
2446
|
-
query: delivery.query,
|
|
2447
|
-
read: delivery.read,
|
|
2448
|
-
surround: delivery.surround,
|
|
2449
|
-
diagnostics: (context) => accessControl.require(context, {
|
|
2450
|
-
action: "document.browse",
|
|
2451
|
-
resource: { kind: "catalog" }
|
|
2452
|
-
}).pipe(Effect.andThen(delivery.diagnostics))
|
|
2453
|
-
});
|
|
2454
|
-
return {
|
|
2455
|
-
documents,
|
|
2456
|
-
maintenance: DocumentMaintenance.of({
|
|
2457
|
-
importSources,
|
|
2458
|
-
mediaReferences: (documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2459
|
-
yield* refreshUnlocked();
|
|
2460
|
-
const current = yield* Ref.get(state);
|
|
2461
|
-
return projectMediaReferences(current.authored, current.published, documentId);
|
|
2462
|
-
})),
|
|
2463
|
-
publish,
|
|
2464
|
-
read,
|
|
2465
|
-
refresh,
|
|
2466
|
-
subscribeMutations,
|
|
2467
|
-
validate
|
|
2468
|
-
}),
|
|
2469
|
-
previews,
|
|
2470
|
-
publishedDocuments: publishedService
|
|
2471
|
-
};
|
|
2472
|
-
});
|
|
2473
|
-
function documentsLayer(configuration, options) {
|
|
2474
|
-
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)))));
|
|
2475
1320
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
1321
|
+
|
|
1322
|
+
//#endregion
|
|
1323
|
+
//#region ../platform-node/src/internal/http/form-ingress.ts
|
|
1324
|
+
function handleFormRequest(request, handle) {
|
|
1325
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
1326
|
+
const ingress = yield* Effect.gen(function* () {
|
|
1327
|
+
const origin = request.headers.get("origin");
|
|
1328
|
+
if (origin && origin !== new URL(request.url).origin) return Response.json({ error: "Origin rejected." }, { status: 403 });
|
|
1329
|
+
if (request.method !== "POST") return new Response(null, { status: 405 });
|
|
1330
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response(null, { status: 415 });
|
|
1331
|
+
const reader = request.body?.getReader();
|
|
1332
|
+
if (!reader) return new Response(null, { status: 400 });
|
|
1333
|
+
const chunks = [];
|
|
1334
|
+
let size = 0;
|
|
1335
|
+
try {
|
|
1336
|
+
while (true) {
|
|
1337
|
+
const chunk = yield* Effect.tryPromise(() => reader.read());
|
|
1338
|
+
if (chunk.done) break;
|
|
1339
|
+
size += chunk.value.byteLength;
|
|
1340
|
+
if (size > 131072) {
|
|
1341
|
+
yield* Effect.tryPromise(() => reader.cancel()).pipe(Effect.ignore);
|
|
1342
|
+
return new Response(null, { status: 413 });
|
|
1343
|
+
}
|
|
1344
|
+
chunks.push(chunk.value);
|
|
1345
|
+
}
|
|
1346
|
+
} finally {
|
|
1347
|
+
reader.releaseLock();
|
|
1348
|
+
}
|
|
1349
|
+
const body = new Uint8Array(size);
|
|
1350
|
+
let offset = 0;
|
|
1351
|
+
for (const chunk of chunks) {
|
|
1352
|
+
body.set(chunk, offset);
|
|
1353
|
+
offset += chunk.byteLength;
|
|
1354
|
+
}
|
|
1355
|
+
return body;
|
|
1356
|
+
}).pipe(Effect.orElseSucceed(() => new Response(null, { status: 400 })));
|
|
1357
|
+
if (ingress instanceof Response) return ingress;
|
|
1358
|
+
return yield* Effect.promise(() => handle(new Request(request.url, {
|
|
1359
|
+
method: "POST",
|
|
1360
|
+
headers: request.headers,
|
|
1361
|
+
body: ingress,
|
|
1362
|
+
signal: request.signal
|
|
1363
|
+
})));
|
|
1364
|
+
}).pipe(Effect.map((response) => {
|
|
1365
|
+
response.headers.set("cache-control", "no-store");
|
|
1366
|
+
return response;
|
|
2480
1367
|
})));
|
|
2481
1368
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
1369
|
+
|
|
1370
|
+
//#endregion
|
|
1371
|
+
//#region ../server/src/Readiness.ts
|
|
1372
|
+
var ReadinessFailed = class extends Schema.TaggedError()("Cms.ReadinessFailed", { cause: Schema.Defect() }) {};
|
|
1373
|
+
var Readiness = class extends Context.Service()("@internal/server/Readiness") {};
|
|
1374
|
+
/** A stale unresolved external change makes the installation unavailable until reconciliation resolves it. */
|
|
1375
|
+
const readinessLayer = Layer.effect(Readiness, Effect.gen(function* () {
|
|
1376
|
+
const journal = yield* ChangeJournal;
|
|
1377
|
+
const check = Effect.gen(function* () {
|
|
1378
|
+
const now = yield* DateTime.now;
|
|
1379
|
+
const pending = yield* journal.pending({
|
|
1380
|
+
before: DateTime.subtractDuration(now, "30 seconds"),
|
|
1381
|
+
limit: 1
|
|
1382
|
+
}).pipe(Effect.mapError((cause) => new ReadinessFailed({ cause })));
|
|
1383
|
+
const oldest = pending.changes[0];
|
|
1384
|
+
if (pending.total === 0) return;
|
|
1385
|
+
if (oldest === void 0) return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("The pending change journal returned no change.") });
|
|
1386
|
+
yield* Effect.logWarning("CMS readiness found a stale pending change.", {
|
|
1387
|
+
action: oldest.action,
|
|
1388
|
+
ageMillis: DateTime.toEpochMillis(now) - DateTime.toEpochMillis(oldest.updatedAt),
|
|
1389
|
+
changeId: oldest.id,
|
|
1390
|
+
phase: "pending"
|
|
2489
1391
|
});
|
|
2490
|
-
return
|
|
2491
|
-
}).pipe(Effect.
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
}
|
|
1392
|
+
return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("A stale change remains pending.") });
|
|
1393
|
+
}).pipe(Effect.withSpan("Installation.readiness"));
|
|
1394
|
+
return Readiness.of({ check });
|
|
1395
|
+
}));
|
|
1396
|
+
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region ../server/src/internal/working-documents/working-document-model.ts
|
|
1399
|
+
const WorkingDocumentGenerationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("WorkingDocumentGenerationId"));
|
|
1400
|
+
const WorkingDocumentUpdateSequence = Schema.Int.check(Schema.isGreaterThan(0)).pipe(Schema.brand("WorkingDocumentUpdateSequence"));
|
|
1401
|
+
const WorkingDocumentHighWatermark = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("WorkingDocumentHighWatermark"));
|
|
1402
|
+
var WorkingDocumentPersistenceError = class extends Schema.TaggedError()("WorkingDocuments.PersistenceError", {
|
|
1403
|
+
operation: Schema.NonEmptyString,
|
|
1404
|
+
cause: Schema.Defect()
|
|
1405
|
+
}) {};
|
|
1406
|
+
var WorkingDocumentSuperseded = class extends Schema.TaggedError()("WorkingDocuments.Superseded", {
|
|
1407
|
+
documentId: DocumentId,
|
|
1408
|
+
expectedGeneration: WorkingDocumentGenerationId,
|
|
1409
|
+
actualGeneration: Schema.optionalKey(WorkingDocumentGenerationId)
|
|
1410
|
+
}) {};
|
|
1411
|
+
var WorkingDocumentConflict = class extends Schema.TaggedError()("WorkingDocuments.Conflict", {
|
|
1412
|
+
documentId: DocumentId,
|
|
1413
|
+
operation: Schema.NonEmptyString,
|
|
1414
|
+
message: Schema.NonEmptyString
|
|
1415
|
+
}) {};
|
|
1416
|
+
|
|
1417
|
+
//#endregion
|
|
1418
|
+
//#region ../server/src/internal/working-documents/working-document.ts
|
|
1419
|
+
const WorkingDocumentRef = Schema.Struct({
|
|
1420
|
+
documentId: DocumentId,
|
|
1421
|
+
generationId: WorkingDocumentGenerationId
|
|
1422
|
+
});
|
|
1423
|
+
var WorkingDocumentInvalid = class extends Schema.TaggedError()("WorkingDocuments.Invalid", {
|
|
1424
|
+
documentId: DocumentId,
|
|
1425
|
+
message: Schema.NonEmptyString,
|
|
1426
|
+
cause: Schema.Defect()
|
|
1427
|
+
}) {};
|
|
1428
|
+
var WorkingDocumentUnavailable = class extends Schema.TaggedError()("WorkingDocuments.Unavailable", {
|
|
1429
|
+
operation: Schema.NonEmptyString,
|
|
1430
|
+
cause: Schema.Defect()
|
|
1431
|
+
}) {};
|
|
1432
|
+
/** An authored or working baseline committed, but the publish workflow still needs recovery. */
|
|
1433
|
+
var WorkingDocumentCheckpointed = class extends Schema.TaggedError()("WorkingDocuments.Checkpointed", {
|
|
1434
|
+
checkpointVersion: DocumentVersion,
|
|
1435
|
+
documentId: DocumentId,
|
|
1436
|
+
result: Schema.optionalKey(DocumentMutationResult),
|
|
1437
|
+
stateVector: Schema.Uint8Array,
|
|
1438
|
+
cause: Schema.Defect()
|
|
1439
|
+
}) {};
|
|
1440
|
+
const WorkingDocumentChange = Schema.Struct({
|
|
1441
|
+
kind: Schema.Literals(["updated", "invalidated"]),
|
|
1442
|
+
ref: WorkingDocumentRef
|
|
1443
|
+
});
|
|
1444
|
+
var WorkingDocuments = class extends Context.Service()("@internal/server/WorkingDocuments") {};
|
|
2514
1445
|
|
|
2515
1446
|
//#endregion
|
|
2516
1447
|
//#region ../document/src/internal/editor/identity-extension.ts
|
|
2517
|
-
/** Ensures Yjs only observes authorable nodes with final, unique identities. */
|
|
2518
1448
|
const EditorNodeIdentity = Extension.create({
|
|
2519
1449
|
name: "cmsNodeIdentity",
|
|
2520
1450
|
priority: 1e4,
|
|
@@ -3063,7 +1993,7 @@ function unwrapFirstParagraph(nodes) {
|
|
|
3063
1993
|
}
|
|
3064
1994
|
function restoreOpaqueNode(value, nodeId) {
|
|
3065
1995
|
const decoded = Schema.decodeUnknownOption(MarkdownNode)(value);
|
|
3066
|
-
if (decoded
|
|
1996
|
+
if (Option.isNone(decoded) || Predicate.isString(decoded.value)) return void 0;
|
|
3067
1997
|
const node = structuredClone(decoded.value);
|
|
3068
1998
|
if (nodeId === void 0) return node;
|
|
3069
1999
|
if (node[0] === null) return [
|
|
@@ -3173,10 +2103,10 @@ const PROPERTIES_NAME = "properties";
|
|
|
3173
2103
|
const PROPS_PREFIX = "props:";
|
|
3174
2104
|
const CMS_PROPS_PREFIX = "cmsProps:";
|
|
3175
2105
|
const editorSchema$1 = createEditorSchema();
|
|
3176
|
-
const decodeEditorNodeId = Schema.
|
|
3177
|
-
const decodeJson$1 = Schema.
|
|
3178
|
-
const decodeJsonObject$1 = Schema.
|
|
3179
|
-
const decodePath = Schema.
|
|
2106
|
+
const decodeEditorNodeId = flow(Schema.decodeUnknownResult(EditorNodeId), Result.getOrThrow);
|
|
2107
|
+
const decodeJson$1 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
|
|
2108
|
+
const decodeJsonObject$1 = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
|
|
2109
|
+
const decodePath = flow(Schema.decodeUnknownResult(Schema.Array(Schema.String)), Result.getOrThrow);
|
|
3180
2110
|
function seedCollaborationFields(document, content) {
|
|
3181
2111
|
const state = extractGranularState(content);
|
|
3182
2112
|
replaceMap(document.getMap(FRONTMATTER_NAME), flatten(state.frontmatter));
|
|
@@ -3220,7 +2150,7 @@ function extractGranularState(content) {
|
|
|
3220
2150
|
const nodeId = optionalEditorNodeId(node.attrs?.cmsNodeId);
|
|
3221
2151
|
if (nodeId === void 0) return;
|
|
3222
2152
|
const values = Object.assign({}, prefixValues(optionalJsonObject(node.attrs?.props), PROPS_PREFIX), prefixValues(optionalJsonObject(node.attrs?.cmsProps), CMS_PROPS_PREFIX));
|
|
3223
|
-
if (Object.
|
|
2153
|
+
if (Object.hasOwn(node.attrs ?? {}, "props") || Object.hasOwn(node.attrs ?? {}, "cmsProps")) properties.set(nodeId, values);
|
|
3224
2154
|
});
|
|
3225
2155
|
return {
|
|
3226
2156
|
frontmatter,
|
|
@@ -3245,7 +2175,7 @@ function flatten(value) {
|
|
|
3245
2175
|
}
|
|
3246
2176
|
function unflatten(value) {
|
|
3247
2177
|
const result = {};
|
|
3248
|
-
for (const [encodedPath, child] of Object.entries(value)) {
|
|
2178
|
+
for (const [encodedPath, child] of Object.entries(value).sort(([left], [right]) => decodePath(JSON.parse(left)).length - decodePath(JSON.parse(right)).length || left.localeCompare(right))) {
|
|
3249
2179
|
const path = decodePath(JSON.parse(encodedPath));
|
|
3250
2180
|
if (path.length === 0) continue;
|
|
3251
2181
|
let current = result;
|
|
@@ -3268,10 +2198,10 @@ function mapValues(map) {
|
|
|
3268
2198
|
return decodeJsonObject$1(Object.fromEntries([...map.entries()].map(([key, value]) => [key, structuredClone(decodeJson$1(value))])));
|
|
3269
2199
|
}
|
|
3270
2200
|
function prefixValues(value, prefix) {
|
|
3271
|
-
return decodeJsonObject$1(Object.fromEntries(Object.entries(value).map(([key, child]) => [`${prefix}${key}`, structuredClone(child)])));
|
|
2201
|
+
return decodeJsonObject$1(Object.fromEntries(Object.entries(flatten(encodeLists(value))).map(([key, child]) => [`${prefix}${key}`, structuredClone(child)])));
|
|
3272
2202
|
}
|
|
3273
2203
|
function prefixedValues(value, prefix) {
|
|
3274
|
-
return decodeJsonObject$1(Object.fromEntries(Object.entries(value).filter(([key]) => key.startsWith(prefix)).map(([key, child]) => [key.slice(prefix.length), structuredClone(child)])));
|
|
2204
|
+
return decodeJsonObject$1(decodeLists(unflatten(Object.fromEntries(Object.entries(value).filter(([key]) => key.startsWith(prefix)).map(([key, child]) => [key.slice(prefix.length).startsWith("[") ? key.slice(prefix.length) : JSON.stringify([key.slice(prefix.length)]), structuredClone(child)])))));
|
|
3275
2205
|
}
|
|
3276
2206
|
function optionalJsonObject(value) {
|
|
3277
2207
|
return value === null || value === void 0 ? {} : structuredClone(decodeJsonObject$1(value));
|
|
@@ -3291,6 +2221,33 @@ function define(target, key, value) {
|
|
|
3291
2221
|
writable: true
|
|
3292
2222
|
});
|
|
3293
2223
|
}
|
|
2224
|
+
const listMarker = "\0cmsList";
|
|
2225
|
+
function encodeLists(value) {
|
|
2226
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
2227
|
+
if (Array.isArray(child) && child.every((item) => isJsonObject(item) && Predicate.isString(item.id))) return [key, {
|
|
2228
|
+
[listMarker]: true,
|
|
2229
|
+
items: Object.fromEntries(child.flatMap((item, order) => isJsonObject(item) && Predicate.isString(item.id) ? [[item.id, {
|
|
2230
|
+
order,
|
|
2231
|
+
value: encodeLists(item)
|
|
2232
|
+
}]] : []))
|
|
2233
|
+
}];
|
|
2234
|
+
return [key, isJsonObject(child) ? encodeLists(child) : child];
|
|
2235
|
+
}));
|
|
2236
|
+
}
|
|
2237
|
+
function decodeLists(value) {
|
|
2238
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
2239
|
+
if (!isJsonObject(child)) return [key, child];
|
|
2240
|
+
if (child[listMarker] === true) return [key, (isJsonObject(child.items) ? Object.entries(child.items) : []).flatMap(([id, item]) => {
|
|
2241
|
+
if (!isJsonObject(item) || !isJsonObject(item.value) || item.value.id !== id || !Predicate.isNumber(item.order)) return [];
|
|
2242
|
+
return [{
|
|
2243
|
+
id,
|
|
2244
|
+
order: item.order,
|
|
2245
|
+
value: decodeLists(item.value)
|
|
2246
|
+
}];
|
|
2247
|
+
}).sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)).map((item) => item.value)];
|
|
2248
|
+
return [key, decodeLists(child)];
|
|
2249
|
+
}));
|
|
2250
|
+
}
|
|
3294
2251
|
|
|
3295
2252
|
//#endregion
|
|
3296
2253
|
//#region ../document/src/internal/collaboration/document.ts
|
|
@@ -3299,9 +2256,9 @@ const METADATA_NAME = "cms";
|
|
|
3299
2256
|
const META_KEY = "meta";
|
|
3300
2257
|
const SOURCE_PATH_KEY = "sourcePath";
|
|
3301
2258
|
const editorSchema = createEditorSchema();
|
|
3302
|
-
const decodeJson = Schema.
|
|
3303
|
-
const decodeJsonObject = Schema.
|
|
3304
|
-
const decodeSourcePath = Schema.
|
|
2259
|
+
const decodeJson = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
|
|
2260
|
+
const decodeJsonObject = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
|
|
2261
|
+
const decodeSourcePath = flow(Schema.decodeUnknownResult(SourcePath), Result.getOrThrow);
|
|
3305
2262
|
function seedCollaborationDocument(sourcePath, seed, meta) {
|
|
3306
2263
|
const document = prosemirrorJSONToYDoc(editorSchema, seed, BODY_NAME);
|
|
3307
2264
|
try {
|
|
@@ -3398,7 +2355,6 @@ function sameBytes$1(left, right) {
|
|
|
3398
2355
|
|
|
3399
2356
|
//#endregion
|
|
3400
2357
|
//#region ../document/src/CollaborationDocument.ts
|
|
3401
|
-
/** Creates the initial collaborative state from the canonical editor projection. */
|
|
3402
2358
|
const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(function* (input) {
|
|
3403
2359
|
const seed = yield* openEditorDocument(input.body);
|
|
3404
2360
|
return yield* Effect.try({
|
|
@@ -3406,7 +2362,6 @@ const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(fu
|
|
|
3406
2362
|
catch: (cause) => invalidCollaboration("Could not create the collaborative editor document.", cause)
|
|
3407
2363
|
});
|
|
3408
2364
|
});
|
|
3409
|
-
/** Renders collaborative state through the canonical editor-to-Markdown encoder. */
|
|
3410
2365
|
const renderCollaborationDocument = Effect.fn("CollaborationDocument.render")(function* (document) {
|
|
3411
2366
|
const inspected = yield* inspectCollaborationDocument(document);
|
|
3412
2367
|
return {
|
|
@@ -3425,31 +2380,26 @@ const captureCollaborationDocument = Effect.fn("CollaborationDocument.capture")(
|
|
|
3425
2380
|
catch: (cause) => invalidCollaboration("Could not capture the collaborative editor document.", cause)
|
|
3426
2381
|
});
|
|
3427
2382
|
});
|
|
3428
|
-
/** Restores a validated, caller-owned document from a full collaboration snapshot. */
|
|
3429
2383
|
const restoreCollaborationDocument = Effect.fn("CollaborationDocument.restore")(function* (snapshot) {
|
|
3430
2384
|
return yield* validateRestoredDocument(yield* Effect.try({
|
|
3431
2385
|
try: () => restoreCollaborationSnapshot(snapshot),
|
|
3432
2386
|
catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
|
|
3433
2387
|
}));
|
|
3434
2388
|
});
|
|
3435
|
-
/** Restores a validated document when only its authoritative full-state update is persisted. */
|
|
3436
2389
|
const restoreCollaborationState = Effect.fn("CollaborationDocument.restoreState")(function* (state) {
|
|
3437
2390
|
return yield* validateRestoredDocument(yield* Effect.try({
|
|
3438
2391
|
try: () => restoreCollaborationUpdate(state),
|
|
3439
2392
|
catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
|
|
3440
2393
|
}));
|
|
3441
2394
|
});
|
|
3442
|
-
/** Applies one encoded collaboration update while translating corrupt data to a typed failure. */
|
|
3443
2395
|
const applyCollaborationUpdate = Effect.fn("CollaborationDocument.applyUpdate")((document, update) => Effect.try({
|
|
3444
2396
|
try: () => applyCollaborationStateUpdate(document, update),
|
|
3445
2397
|
catch: (cause) => invalidCollaboration("Could not apply the collaborative document update.", cause)
|
|
3446
2398
|
}));
|
|
3447
|
-
/** Captures the current state vector and an optional differential update. */
|
|
3448
2399
|
const synchronizeCollaborationState = Effect.fn("CollaborationDocument.synchronizeState")((document, stateVector) => Effect.try({
|
|
3449
2400
|
try: () => synchronizeCollaborationStateValue(document, stateVector),
|
|
3450
2401
|
catch: (cause) => invalidCollaboration("Could not synchronize the collaborative document state.", cause)
|
|
3451
2402
|
}));
|
|
3452
|
-
/** Releases a collaboration document owned by a scoped workflow. */
|
|
3453
2403
|
function destroyCollaborationDocument(document) {
|
|
3454
2404
|
return Effect.sync(() => document.destroy());
|
|
3455
2405
|
}
|
|
@@ -3550,6 +2500,25 @@ function sameJson(left, right) {
|
|
|
3550
2500
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
3551
2501
|
}
|
|
3552
2502
|
|
|
2503
|
+
//#endregion
|
|
2504
|
+
//#region ../server/src/internal/working-documents/working-document-source.ts
|
|
2505
|
+
const validatedWorkingDocument = Effect.fn("WorkingDocuments.validateSource")(function* (source, validate) {
|
|
2506
|
+
const validation = yield* validate(UpdateDocumentInput.make({
|
|
2507
|
+
documentId: source.documentId,
|
|
2508
|
+
expectedVersion: source.version,
|
|
2509
|
+
markdown: source.markdown
|
|
2510
|
+
}));
|
|
2511
|
+
if (!validation.valid || !validation.document) return yield* new WorkingDocumentInvalid({
|
|
2512
|
+
cause: validation.issues,
|
|
2513
|
+
documentId: source.documentId,
|
|
2514
|
+
message: "Authored document cannot initialize a Working Document."
|
|
2515
|
+
});
|
|
2516
|
+
return validation.document;
|
|
2517
|
+
});
|
|
2518
|
+
const newWorkingSnapshot = Effect.fn("WorkingDocuments.newSnapshot")(function* (source, document, generationId) {
|
|
2519
|
+
return yield* createWorkingSnapshot(source, document, yield* generationId(source.documentId));
|
|
2520
|
+
});
|
|
2521
|
+
|
|
3553
2522
|
//#endregion
|
|
3554
2523
|
//#region ../server/src/internal/working-documents/working-document-reconciliation.ts
|
|
3555
2524
|
const isDocumentNotFound$3 = Schema.is(DocumentNotFound);
|
|
@@ -3564,18 +2533,6 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
|
|
|
3564
2533
|
})
|
|
3565
2534
|
}).pipe(Effect.asVoid);
|
|
3566
2535
|
const currentSource = (documentId) => options.documents.read(documentId).pipe(Effect.asSome, Effect.catchIf(isDocumentNotFound$3, () => Effect.succeedNone));
|
|
3567
|
-
const validatedDocument = Effect.fn("WorkingDocuments.reconcile.validateSource")(function* (source) {
|
|
3568
|
-
const validation = yield* options.documents.validate(UpdateDocumentInput.make({
|
|
3569
|
-
documentId: source.documentId,
|
|
3570
|
-
expectedVersion: source.version,
|
|
3571
|
-
markdown: source.markdown
|
|
3572
|
-
}));
|
|
3573
|
-
if (!validation.valid || !validation.document) return yield* invalidSource(source.documentId, validation);
|
|
3574
|
-
return validation.document;
|
|
3575
|
-
});
|
|
3576
|
-
const newSnapshot = Effect.fn("WorkingDocuments.reconcile.newSnapshot")(function* (source, document) {
|
|
3577
|
-
return yield* createWorkingSnapshot(source, document, yield* options.generationId(source.documentId));
|
|
3578
|
-
});
|
|
3579
2536
|
const rotateDirty = Effect.fn("WorkingDocuments.reconcile.rotateDirty")(function* (snapshot) {
|
|
3580
2537
|
const generationId = yield* options.generationId(snapshot.documentId);
|
|
3581
2538
|
const seed = yield* useWorkingSnapshot(snapshot, (document) => captureCollaborationDocument(document).pipe(Effect.map((captured) => rotatedSeed(snapshot, generationId, captured))));
|
|
@@ -3611,10 +2568,10 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
|
|
|
3611
2568
|
yield* publishChange(loaded.value);
|
|
3612
2569
|
return;
|
|
3613
2570
|
}
|
|
3614
|
-
const document = yield*
|
|
2571
|
+
const document = yield* validatedWorkingDocument(source.value, options.documents.validate);
|
|
3615
2572
|
yield* options.store.replace({
|
|
3616
2573
|
expectedGeneration: loaded.value.generationId,
|
|
3617
|
-
snapshot: yield*
|
|
2574
|
+
snapshot: yield* newWorkingSnapshot(source.value, document, options.generationId)
|
|
3618
2575
|
});
|
|
3619
2576
|
yield* publishChange(loaded.value);
|
|
3620
2577
|
}));
|
|
@@ -3644,6 +2601,7 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
|
|
|
3644
2601
|
return [true, {
|
|
3645
2602
|
...current,
|
|
3646
2603
|
mutation: result,
|
|
2604
|
+
sweepRequired: current.sweepRequired || (result.affectedDocumentIds?.length ?? 0) > 1,
|
|
3647
2605
|
target: result.generation
|
|
3648
2606
|
}];
|
|
3649
2607
|
})) yield* Queue.offer(wakeups, void 0);
|
|
@@ -3699,13 +2657,6 @@ function rotatedSeed(snapshot, generationId, captured) {
|
|
|
3699
2657
|
state: Uint8Array.from(captured.state)
|
|
3700
2658
|
};
|
|
3701
2659
|
}
|
|
3702
|
-
function invalidSource(documentId, validation) {
|
|
3703
|
-
return Effect.fail(new WorkingDocumentInvalid({
|
|
3704
|
-
cause: validation.issues,
|
|
3705
|
-
documentId,
|
|
3706
|
-
message: "Authored document cannot initialize a Working Document."
|
|
3707
|
-
}));
|
|
3708
|
-
}
|
|
3709
2660
|
function generationAtOrBefore(candidate, current) {
|
|
3710
2661
|
return BigInt(candidate) <= BigInt(current);
|
|
3711
2662
|
}
|
|
@@ -3713,6 +2664,28 @@ function generationsAreAdjacent(current, candidate) {
|
|
|
3713
2664
|
return BigInt(candidate) === BigInt(current) + 1n;
|
|
3714
2665
|
}
|
|
3715
2666
|
|
|
2667
|
+
//#endregion
|
|
2668
|
+
//#region ../server/src/internal/working-documents/document-locks.ts
|
|
2669
|
+
/** Retain a lock only while an operation is running or waiting for that document. */
|
|
2670
|
+
function documentSynchronization() {
|
|
2671
|
+
const locks = /* @__PURE__ */ new Map();
|
|
2672
|
+
return (documentId, effect) => Effect.acquireUseRelease(Effect.sync(() => {
|
|
2673
|
+
let lock = locks.get(documentId);
|
|
2674
|
+
if (!lock) {
|
|
2675
|
+
lock = {
|
|
2676
|
+
semaphore: Semaphore.makeUnsafe(1),
|
|
2677
|
+
users: 0
|
|
2678
|
+
};
|
|
2679
|
+
locks.set(documentId, lock);
|
|
2680
|
+
}
|
|
2681
|
+
lock.users += 1;
|
|
2682
|
+
return lock;
|
|
2683
|
+
}), (lock) => lock.semaphore.withPermit(effect), (lock) => Effect.sync(() => {
|
|
2684
|
+
lock.users -= 1;
|
|
2685
|
+
if (lock.users === 0 && locks.get(documentId) === lock) locks.delete(documentId);
|
|
2686
|
+
}));
|
|
2687
|
+
}
|
|
2688
|
+
|
|
3716
2689
|
//#endregion
|
|
3717
2690
|
//#region ../server/src/internal/working-documents/working-documents.ts
|
|
3718
2691
|
function workingDocumentsLayer(options = {}) {
|
|
@@ -3726,31 +2699,11 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
|
|
|
3726
2699
|
const store = yield* WorkingDocumentStore;
|
|
3727
2700
|
const changeSource = yield* WorkingDocumentChanges;
|
|
3728
2701
|
const changes = yield* Effect.acquireRelease(PubSub.bounded(64), PubSub.shutdown);
|
|
3729
|
-
const
|
|
2702
|
+
const synchronized = documentSynchronization();
|
|
3730
2703
|
const compactAfterUpdates = validCompactionLimit(options.compactAfterUpdates);
|
|
3731
|
-
const synchronized = (documentId, effect) => Effect.suspend(() => {
|
|
3732
|
-
let semaphore = locks.get(documentId);
|
|
3733
|
-
if (!semaphore) {
|
|
3734
|
-
semaphore = Semaphore.makeUnsafe(1);
|
|
3735
|
-
locks.set(documentId, semaphore);
|
|
3736
|
-
}
|
|
3737
|
-
return semaphore.withPermit(effect);
|
|
3738
|
-
});
|
|
3739
2704
|
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)));
|
|
3740
2705
|
const attemptId = (documentId) => crypto.randomUUIDv4.pipe(Effect.map((uuid) => WorkingDocumentPublishAttemptId.make(uuid)), Effect.mapError((cause) => invalid(documentId, "Could not create a publish attempt.", cause)));
|
|
3741
2706
|
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)));
|
|
3742
|
-
const validatedDocument = Effect.fn("WorkingDocuments.validateSource")(function* (source) {
|
|
3743
|
-
const validation = yield* maintenance.validate(UpdateDocumentInput.make({
|
|
3744
|
-
documentId: source.documentId,
|
|
3745
|
-
expectedVersion: source.version,
|
|
3746
|
-
markdown: source.markdown
|
|
3747
|
-
}));
|
|
3748
|
-
if (!validation.valid || !validation.document) return yield* invalid(source.documentId, "Authored document cannot initialize a Working Document.", validation.issues);
|
|
3749
|
-
return validation.document;
|
|
3750
|
-
});
|
|
3751
|
-
const newSnapshot = Effect.fn("WorkingDocuments.newSnapshot")(function* (source, document) {
|
|
3752
|
-
return yield* createWorkingSnapshot(source, document, yield* generationId(source.documentId));
|
|
3753
|
-
});
|
|
3754
2707
|
const compactSnapshot = Effect.fn("WorkingDocuments.compactSnapshot")(function* (snapshot) {
|
|
3755
2708
|
const lastUpdate = snapshot.updates.at(-1);
|
|
3756
2709
|
if (!lastUpdate) return false;
|
|
@@ -3830,21 +2783,21 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
|
|
|
3830
2783
|
const ensureOpenSnapshot = Effect.fn("WorkingDocuments.ensureOpenSnapshot")(function* (source) {
|
|
3831
2784
|
const loaded = yield* store.load(source.documentId);
|
|
3832
2785
|
if (Option.isNone(loaded)) {
|
|
3833
|
-
const document = yield*
|
|
3834
|
-
const initialized = yield* store.initialize(yield*
|
|
2786
|
+
const document = yield* validatedWorkingDocument(source, maintenance.validate);
|
|
2787
|
+
const initialized = yield* store.initialize(yield* newWorkingSnapshot(source, document, generationId));
|
|
3835
2788
|
if (initialized.savedVersion === source.version) return initialized;
|
|
3836
2789
|
if (yield* workingSnapshotIsDirty(initialized)) return initialized;
|
|
3837
2790
|
return yield* store.replace({
|
|
3838
2791
|
expectedGeneration: initialized.generationId,
|
|
3839
|
-
snapshot: yield*
|
|
2792
|
+
snapshot: yield* newWorkingSnapshot(source, document, generationId)
|
|
3840
2793
|
});
|
|
3841
2794
|
}
|
|
3842
2795
|
if (loaded.value.savedVersion === source.version) return loaded.value;
|
|
3843
2796
|
if (yield* workingSnapshotIsDirty(loaded.value)) return loaded.value;
|
|
3844
|
-
const document = yield*
|
|
2797
|
+
const document = yield* validatedWorkingDocument(source, maintenance.validate);
|
|
3845
2798
|
return yield* store.replace({
|
|
3846
2799
|
expectedGeneration: loaded.value.generationId,
|
|
3847
|
-
snapshot: yield*
|
|
2800
|
+
snapshot: yield* newWorkingSnapshot(source, document, generationId)
|
|
3848
2801
|
});
|
|
3849
2802
|
});
|
|
3850
2803
|
const open = (context, documentId) => synchronized(documentId, Effect.gen(function* () {
|
|
@@ -4068,7 +3021,7 @@ function closeHocuspocus(hocuspocus, operations) {
|
|
|
4068
3021
|
const store = hocuspocus.debouncer.executeNow(`onStoreDocument-${document.name}`);
|
|
4069
3022
|
return store === void 0 ? [] : [store];
|
|
4070
3023
|
});
|
|
4071
|
-
yield* Effect.promise(() => Promise.allSettled(stores));
|
|
3024
|
+
const storeResults = yield* Effect.promise(() => Promise.allSettled(stores));
|
|
4072
3025
|
yield* Effect.promise(operations.drain);
|
|
4073
3026
|
const cleanupResults = yield* Effect.promise(() => Promise.allSettled(documents.map((document) => document.saveMutex.runExclusive(() => Promise.resolve()).then(() => hocuspocus.unloadDocument(document)))));
|
|
4074
3027
|
const destroyResult = yield* Effect.result(Effect.tryPromise({
|
|
@@ -4076,6 +3029,8 @@ function closeHocuspocus(hocuspocus, operations) {
|
|
|
4076
3029
|
catch: (cause) => gatewayError("close", cause)
|
|
4077
3030
|
}).pipe(Effect.asVoid));
|
|
4078
3031
|
const cleanupFailure = cleanupResults.find((result) => result.status === "rejected");
|
|
3032
|
+
const storeFailure = storeResults.find((result) => result.status === "rejected");
|
|
3033
|
+
if (storeFailure?.status === "rejected") return yield* gatewayError("close", storeFailure.reason);
|
|
4079
3034
|
if (cleanupFailure?.status === "rejected") return yield* gatewayError("close", cleanupFailure.reason);
|
|
4080
3035
|
if (Result.isFailure(destroyResult)) return yield* destroyResult.failure;
|
|
4081
3036
|
});
|
|
@@ -4089,7 +3044,6 @@ function gatewayError(operation, cause) {
|
|
|
4089
3044
|
|
|
4090
3045
|
//#endregion
|
|
4091
3046
|
//#region ../platform-node/src/internal/realtime/keyed-operations.ts
|
|
4092
|
-
/** Serializes each room independently and retains poisoned writes for shutdown reporting. */
|
|
4093
3047
|
function createKeyedOperations(options) {
|
|
4094
3048
|
const pending = /* @__PURE__ */ new Map();
|
|
4095
3049
|
const failures = /* @__PURE__ */ new Map();
|
|
@@ -4183,7 +3137,6 @@ function stableStringHash(value) {
|
|
|
4183
3137
|
//#region ../platform-node/src/internal/realtime/wake-dispatcher.ts
|
|
4184
3138
|
const DEFAULT_CAPACITY = 1024;
|
|
4185
3139
|
const DEFAULT_CONCURRENCY = 32;
|
|
4186
|
-
/** Bounded wake scheduling: duplicate rooms coalesce and unrelated rooms run concurrently. */
|
|
4187
3140
|
const runStudioRealtimeWakeDispatcher = Effect.fn("StudioRealtimeWakeDispatcher.run")(function* (wakes, handle, options = {}) {
|
|
4188
3141
|
const queue = yield* Queue.dropping(options.capacity ?? DEFAULT_CAPACITY);
|
|
4189
3142
|
const queued = /* @__PURE__ */ new Set();
|
|
@@ -4396,7 +3349,7 @@ const makeStudioRealtimeRoomHost = Effect.fn("StudioRealtimeRoomHost.make")(func
|
|
|
4396
3349
|
},
|
|
4397
3350
|
onStateless: (payload) => runTracked(Effect.gen(function* () {
|
|
4398
3351
|
const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
|
|
4399
|
-
if (decoded
|
|
3352
|
+
if (Option.isNone(decoded) || decoded.value.type !== "checkpoint") return;
|
|
4400
3353
|
const name = yield* decodeRoomName(payload.documentName).pipe(Effect.mapError((cause) => roomHostError("checkpoint", cause)));
|
|
4401
3354
|
const stateVector = Y.encodeStateVector(payload.document);
|
|
4402
3355
|
yield* Effect.tryPromise({
|
|
@@ -4529,7 +3482,9 @@ function decodeBytes(value, tokenInvalid) {
|
|
|
4529
3482
|
try: () => {
|
|
4530
3483
|
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
4531
3484
|
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
4532
|
-
|
|
3485
|
+
const bytes = Uint8Array.from(binary, (character) => character.codePointAt(0) ?? 0);
|
|
3486
|
+
if (encodeBytes(bytes) !== value) throw new Error("Noncanonical base64url encoding");
|
|
3487
|
+
return bytes;
|
|
4533
3488
|
},
|
|
4534
3489
|
catch: tokenInvalid
|
|
4535
3490
|
});
|
|
@@ -4671,7 +3626,7 @@ const makeStudioRealtimeGateway = Effect.gen(function* () {
|
|
|
4671
3626
|
onStateless: (payload) => {
|
|
4672
3627
|
return runTracked(Effect.gen(function* () {
|
|
4673
3628
|
const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
|
|
4674
|
-
if (decoded
|
|
3629
|
+
if (Option.isNone(decoded)) return;
|
|
4675
3630
|
const request = decoded.value;
|
|
4676
3631
|
const context = authenticatedContext(payload.connection.context, payload.documentName);
|
|
4677
3632
|
if (request.type === "checkpoint") return;
|
|
@@ -4881,11 +3836,10 @@ function httpProtocol(protocol) {
|
|
|
4881
3836
|
* StudioRealtimeRoomHost synchronizes durable Yjs state across replicas. Presence
|
|
4882
3837
|
* and stateless broadcasts remain process-local until an ephemeral bus is added.
|
|
4883
3838
|
*/
|
|
4884
|
-
const studioRealtimeGatewayLayer = Layer.effect(StudioRealtimeGateway, Effect.acquireRelease(makeStudioRealtimeGateway, (gateway) => gateway.close.pipe(Effect.
|
|
3839
|
+
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)));
|
|
4885
3840
|
|
|
4886
3841
|
//#endregion
|
|
4887
3842
|
//#region ../platform-node/src/internal/realtime/websocket.ts
|
|
4888
|
-
/** Hides Hocuspocus connection bookkeeping from framework WebSocket adapters. */
|
|
4889
3843
|
function studioRealtimeWebSocketBridge(gateway) {
|
|
4890
3844
|
const connections = /* @__PURE__ */ new WeakMap();
|
|
4891
3845
|
const release = (socket) => {
|
|
@@ -4910,21 +3864,25 @@ function studioRealtimeWebSocketBridge(gateway) {
|
|
|
4910
3864
|
//#endregion
|
|
4911
3865
|
//#region ../platform-node/src/internal/installation/host.ts
|
|
4912
3866
|
var Installation = class extends Context.Service()("@internal/platform-node/Installation") {};
|
|
4913
|
-
function installationHttpLayer(bootstrap,
|
|
3867
|
+
function installationHttpLayer(bootstrap, cleanup) {
|
|
4914
3868
|
return Layer.effect(Installation, Effect.gen(function* () {
|
|
4915
|
-
const outerContext = yield* Effect.context();
|
|
4916
3869
|
const cleanupContext = yield* Effect.context();
|
|
4917
|
-
const
|
|
4918
|
-
const
|
|
4919
|
-
|
|
3870
|
+
const routerConfig = yield* HttpRouter.RouterConfig;
|
|
3871
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
3872
|
+
const handle = HttpEffect.toWebHandlerWith(yield* Effect.context())(router.asHttpEffect(), HttpMiddleware.logger);
|
|
3873
|
+
return yield* makeInstallation(bootstrap, handle, (context) => cleanup(context).pipe(Effect.provide(cleanupContext)), routerConfig);
|
|
4920
3874
|
}));
|
|
4921
3875
|
}
|
|
4922
|
-
const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup) {
|
|
3876
|
+
const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup, routerConfig) {
|
|
4923
3877
|
const authentication = yield* AuthenticationHandler;
|
|
4924
3878
|
const crypto = yield* Crypto.Crypto;
|
|
4925
3879
|
const realtime = yield* StudioRealtimeGateway;
|
|
4926
3880
|
const roomHost = yield* StudioRealtimeRoomHost;
|
|
4927
|
-
const
|
|
3881
|
+
const readiness = yield* Readiness;
|
|
3882
|
+
const protectedRoutes = FindMyWay.make(routerConfig);
|
|
3883
|
+
protectedRoutes.all(CMS_FORM_SUBMIT_PATH, "form");
|
|
3884
|
+
protectedRoutes.all(`${STUDIO_API_PATH}/preview/*`, "preview");
|
|
3885
|
+
const health = readiness.check.pipe(Effect.andThen(roomHost.check), Effect.match({
|
|
4928
3886
|
onFailure: () => ({
|
|
4929
3887
|
live: true,
|
|
4930
3888
|
ready: false
|
|
@@ -4950,12 +3908,18 @@ const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, ha
|
|
|
4950
3908
|
cleanupMedia,
|
|
4951
3909
|
fetch: (request) => {
|
|
4952
3910
|
const pathname = new URL(request.url).pathname;
|
|
3911
|
+
const protection = protectedRoutes.find(request.method, pathname)?.handler;
|
|
3912
|
+
if (protection === "form") return handleFormRequest(request, handle);
|
|
4953
3913
|
if (pathname === "/api/cms/auth" || pathname.startsWith(`${"/api/cms/auth"}/`)) return authentication.handle(request);
|
|
4954
3914
|
if (pathname === CMS_LIVENESS_PATH) return Promise.resolve(healthResponse({
|
|
4955
3915
|
live: true,
|
|
4956
3916
|
ready: true
|
|
4957
3917
|
}, false));
|
|
4958
3918
|
if (pathname === CMS_READINESS_PATH) return runPromise(health).then((status) => healthResponse(status, true));
|
|
3919
|
+
if (protection === "preview") return handle(request).then((response) => {
|
|
3920
|
+
response.headers.set("cache-control", "private, no-store");
|
|
3921
|
+
return response;
|
|
3922
|
+
});
|
|
4959
3923
|
return handle(request);
|
|
4960
3924
|
},
|
|
4961
3925
|
health,
|
|
@@ -5000,21 +3964,6 @@ const startInstallation = Effect.fn("CmsInstallation.start")(function* (options)
|
|
|
5000
3964
|
};
|
|
5001
3965
|
});
|
|
5002
3966
|
|
|
5003
|
-
//#endregion
|
|
5004
|
-
//#region ../server/src/internal/media/media-object-store.ts
|
|
5005
|
-
var MediaObjectStore = class extends Context.Service()("@internal/server/MediaObjectStore") {};
|
|
5006
|
-
|
|
5007
|
-
//#endregion
|
|
5008
|
-
//#region ../server/src/internal/media/media-failures.ts
|
|
5009
|
-
var MediaPersistenceError = class extends Schema.TaggedError()("Media.PersistenceError", {
|
|
5010
|
-
operation: Schema.NonEmptyString,
|
|
5011
|
-
cause: Schema.Defect()
|
|
5012
|
-
}) {};
|
|
5013
|
-
var MediaStorageError = class extends Schema.TaggedError()("Media.StorageError", {
|
|
5014
|
-
operation: Schema.NonEmptyString,
|
|
5015
|
-
cause: Schema.Defect()
|
|
5016
|
-
}) {};
|
|
5017
|
-
|
|
5018
3967
|
//#endregion
|
|
5019
3968
|
//#region ../server/src/internal/media/media-change.ts
|
|
5020
3969
|
const MediaUploadIntent = Schema.Struct({
|
|
@@ -5177,7 +4126,7 @@ function makeMediaChange(input) {
|
|
|
5177
4126
|
action: input.action,
|
|
5178
4127
|
context: input.change.context,
|
|
5179
4128
|
id: input.change.id,
|
|
5180
|
-
intent: Schema.
|
|
4129
|
+
intent: Result.getOrThrow(Schema.encodeResult(MediaChangeIntent)(input.intent)),
|
|
5181
4130
|
scope: "media",
|
|
5182
4131
|
subject: input.subject,
|
|
5183
4132
|
target: input.target
|
|
@@ -5186,55 +4135,43 @@ function makeMediaChange(input) {
|
|
|
5186
4135
|
|
|
5187
4136
|
//#endregion
|
|
5188
4137
|
//#region ../server/src/internal/media/media-reconciliation.ts
|
|
5189
|
-
|
|
4138
|
+
const recoveryBatchSize = 100;
|
|
4139
|
+
function recoverPendingMedia(store, objectStore, operations, recoveryAge, cursor = {}) {
|
|
5190
4140
|
return Effect.gen(function* () {
|
|
5191
4141
|
const before = DateTime.subtractDuration(yield* DateTime.now, recoveryAge);
|
|
5192
|
-
const
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
4142
|
+
const pending = yield* store.pending({
|
|
4143
|
+
after: cursor.media,
|
|
4144
|
+
before,
|
|
4145
|
+
limit: recoveryBatchSize
|
|
4146
|
+
});
|
|
4147
|
+
for (const record of pending) {
|
|
4148
|
+
if (record.status === "deleting") {
|
|
4149
|
+
yield* objectStore.remove(record.objectKey);
|
|
4150
|
+
yield* store.finishDelete(record.id);
|
|
4151
|
+
continue;
|
|
4152
|
+
}
|
|
4153
|
+
const stored = yield* objectStore.stat(record.objectKey);
|
|
4154
|
+
if (Option.isSome(stored) && stored.value.byteSize === record.byteSize && (stored.value.mimeType === void 0 || stored.value.mimeType === record.mimeType)) yield* store.activate(record.id);
|
|
4155
|
+
else {
|
|
4156
|
+
if (Option.isSome(stored)) {
|
|
5200
4157
|
yield* objectStore.remove(record.objectKey);
|
|
5201
|
-
yield*
|
|
5202
|
-
continue;
|
|
5203
|
-
}
|
|
5204
|
-
const stored = yield* objectStore.stat(record.objectKey);
|
|
5205
|
-
if (Option.isSome(stored) && stored.value.byteSize === record.byteSize && (stored.value.mimeType === void 0 || stored.value.mimeType === record.mimeType)) yield* store.activate(record.id);
|
|
5206
|
-
else {
|
|
5207
|
-
if (Option.isSome(stored)) {
|
|
5208
|
-
yield* objectStore.remove(record.objectKey);
|
|
5209
|
-
const remaining = yield* objectStore.stat(record.objectKey);
|
|
5210
|
-
if (Option.isSome(remaining)) continue;
|
|
5211
|
-
}
|
|
5212
|
-
yield* store.abandonUpload(record.id);
|
|
4158
|
+
const remaining = yield* objectStore.stat(record.objectKey);
|
|
4159
|
+
if (Option.isSome(remaining)) continue;
|
|
5213
4160
|
}
|
|
4161
|
+
yield* store.abandonUpload(record.id);
|
|
5214
4162
|
}
|
|
5215
|
-
return pending.length < 100;
|
|
5216
|
-
});
|
|
5217
|
-
let mediaBatch = 0;
|
|
5218
|
-
let mediaComplete = false;
|
|
5219
|
-
while (!mediaComplete && mediaBatch < maxBatches) {
|
|
5220
|
-
mediaComplete = yield* recoverBatch;
|
|
5221
|
-
mediaBatch += 1;
|
|
5222
4163
|
}
|
|
5223
|
-
const
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
});
|
|
5228
|
-
yield* Effect.forEach(replacements, (replacement) => recoverReplacement(store, objectStore, replacement).pipe(Effect.catchTag("Media.Conflict", () => Effect.void)), { discard: true });
|
|
5229
|
-
return replacements.length < 100;
|
|
4164
|
+
const replacements = yield* store.pendingReplacements({
|
|
4165
|
+
after: cursor.replacements,
|
|
4166
|
+
before,
|
|
4167
|
+
limit: recoveryBatchSize
|
|
5230
4168
|
});
|
|
5231
|
-
|
|
5232
|
-
let replacementsComplete = false;
|
|
5233
|
-
while (!replacementsComplete && replacementBatch < maxBatches) {
|
|
5234
|
-
replacementsComplete = yield* recoverReplacementBatch;
|
|
5235
|
-
replacementBatch += 1;
|
|
5236
|
-
}
|
|
4169
|
+
yield* Effect.forEach(replacements, (replacement) => recoverReplacement(store, objectStore, replacement).pipe(Effect.catchTag("Media.Conflict", () => Effect.void)), { discard: true });
|
|
5237
4170
|
yield* recoverPendingMediaCleanup(operations, objectStore, before);
|
|
4171
|
+
return {
|
|
4172
|
+
media: pending.length < recoveryBatchSize ? void 0 : pending.at(-1)?.id,
|
|
4173
|
+
replacements: replacements.length < recoveryBatchSize ? void 0 : replacements.at(-1)?.mediaId
|
|
4174
|
+
};
|
|
5238
4175
|
});
|
|
5239
4176
|
}
|
|
5240
4177
|
function recoverPendingMediaCleanup(operations, objectStore, before) {
|
|
@@ -5422,10 +4359,14 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
|
|
|
5422
4359
|
const store = yield* MediaStore;
|
|
5423
4360
|
const baseUrl = options.publicBaseUrl.replace(/\/+$/gu, "");
|
|
5424
4361
|
const pendingRecoveryAge = options.pendingRecoveryAge ?? "5 minutes";
|
|
5425
|
-
yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge);
|
|
4362
|
+
const recoveryCursor = yield* Ref.make(yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge));
|
|
5426
4363
|
const mutations = yield* Semaphore.make(1);
|
|
5427
4364
|
const recoveryInterval = options.pendingRecoveryInterval ?? MEDIA_OPERATION_RECOVERY_INTERVAL;
|
|
5428
|
-
if (recoveryInterval !== false) yield* mutations.withPermits(1)(
|
|
4365
|
+
if (recoveryInterval !== false) yield* mutations.withPermits(1)(Effect.gen(function* () {
|
|
4366
|
+
const cursor = yield* Ref.get(recoveryCursor);
|
|
4367
|
+
const next = yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge, cursor);
|
|
4368
|
+
yield* Ref.set(recoveryCursor, next);
|
|
4369
|
+
})).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 }));
|
|
5429
4370
|
const toAsset = (record) => MediaAsset.make({
|
|
5430
4371
|
byteSize: record.byteSize,
|
|
5431
4372
|
createdAt: record.createdAt,
|
|
@@ -5494,7 +4435,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
|
|
|
5494
4435
|
return yield* Effect.scoped(prepareUpload(input).pipe(Effect.flatMap((prepared) => mutations.withPermits(1)(Effect.gen(function* () {
|
|
5495
4436
|
const id = MediaId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
5496
4437
|
const operationId = ActivityId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
5497
|
-
const objectKey = MediaObjectKey.make(
|
|
4438
|
+
const objectKey = MediaObjectKey.make(`${mediaObjectPrefix}${id}.${imageExtension(prepared.mimeType)}`);
|
|
5498
4439
|
const now = yield* DateTime.now;
|
|
5499
4440
|
const reserved = yield* store.reserve({
|
|
5500
4441
|
byteSize: input.metadata.declaredByteSize,
|
|
@@ -5542,7 +4483,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
|
|
|
5542
4483
|
name: input.metadata.name,
|
|
5543
4484
|
objectKey: current.value.objectKey,
|
|
5544
4485
|
staged: false,
|
|
5545
|
-
stagingObjectKey: MediaObjectKey.make(
|
|
4486
|
+
stagingObjectKey: MediaObjectKey.make(`${mediaObjectPrefix}replacements/${operationId}.${imageExtension(prepared.mimeType)}`),
|
|
5546
4487
|
updatedAt: timestamp
|
|
5547
4488
|
};
|
|
5548
4489
|
const pendingReplacement = yield* store.beginReplace(replacement, {
|
|
@@ -5673,62 +4614,34 @@ function activityFailure(operation) {
|
|
|
5673
4614
|
}
|
|
5674
4615
|
|
|
5675
4616
|
//#endregion
|
|
5676
|
-
//#region ../server/src/
|
|
5677
|
-
|
|
5678
|
-
const
|
|
5679
|
-
const
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
return Layer.
|
|
5690
|
-
}
|
|
5691
|
-
|
|
5692
|
-
const
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
});
|
|
5699
|
-
const issue = Effect.fn("PreviewSessions.issue")(function* (actorId, editorOrigin, previewOrigin) {
|
|
5700
|
-
if (!options.allowedPreviewOrigins.has(previewOrigin)) return yield* invalidSession("origin");
|
|
5701
|
-
const expiresAt = (yield* Clock.currentTimeMillis) + lifetimeMillis;
|
|
5702
|
-
const claims = PreviewSessionClaims.make({
|
|
5703
|
-
actorId,
|
|
5704
|
-
editorOrigin,
|
|
5705
|
-
expiresAt,
|
|
5706
|
-
previewOrigin,
|
|
5707
|
-
sessionId: PreviewSessionId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie))
|
|
5708
|
-
});
|
|
5709
|
-
const token = yield* Schema.encodeEffect(Schema.fromJsonString(PreviewSessionClaims))(claims).pipe(Effect.mapError(() => new PreviewSessionInvalid({ reason: "invalid" })), Effect.flatMap(tokens.sign), Effect.mapError(() => new PreviewSessionInvalid({ reason: "invalid" })));
|
|
5710
|
-
return PreviewSession.make({
|
|
5711
|
-
editorOrigin,
|
|
5712
|
-
expiresAt: DateTime.makeUnsafe(expiresAt),
|
|
5713
|
-
previewOrigin,
|
|
5714
|
-
sessionId: claims.sessionId,
|
|
5715
|
-
token
|
|
5716
|
-
});
|
|
5717
|
-
});
|
|
5718
|
-
const verify = Effect.fn("PreviewSessions.verify")(function* (token, previewOrigin) {
|
|
5719
|
-
const payload = yield* tokens.verify(token, MAXIMUM_TOKEN_LENGTH).pipe(Effect.mapError(() => new PreviewSessionInvalid({ reason: "invalid" })));
|
|
5720
|
-
const claims = yield* Schema.decodeEffect(Schema.fromJsonString(PreviewSessionClaims))(payload).pipe(Effect.mapError(() => new PreviewSessionInvalid({ reason: "invalid" })));
|
|
5721
|
-
if (claims.previewOrigin !== previewOrigin) return yield* invalidSession("origin");
|
|
5722
|
-
if (claims.expiresAt <= (yield* Clock.currentTimeMillis)) return yield* invalidSession("expired");
|
|
5723
|
-
return claims;
|
|
5724
|
-
});
|
|
5725
|
-
return PreviewSessions.of({
|
|
5726
|
-
issue,
|
|
5727
|
-
verify
|
|
4617
|
+
//#region ../server/src/ProductModules.ts
|
|
4618
|
+
function productModulesLayer(options) {
|
|
4619
|
+
const accessControl = defaultAccessControlLayer;
|
|
4620
|
+
const seo = seoLayer(options.bootstrap.seo ?? makeDefaultSeoOptions(options.baseUrl)).pipe(Layer.provide(accessControl));
|
|
4621
|
+
const authoredDocuments = documentsLayer(options.bootstrap.documents, {
|
|
4622
|
+
...options.documentEngine,
|
|
4623
|
+
components: options.documentEngine.components ?? [],
|
|
4624
|
+
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? "https://media.invalid"
|
|
4625
|
+
}).pipe(Layer.provide(accessControl));
|
|
4626
|
+
const documentAccess = Layer.merge(authoredDocuments, accessControl);
|
|
4627
|
+
const activity = activityLayer.pipe(Layer.provide(accessControl));
|
|
4628
|
+
const workingDocuments = workingDocumentsLayer(options.workingDocuments).pipe(Layer.provide(documentAccess));
|
|
4629
|
+
const seoDelivery = seoDeliveryLayer.pipe(Layer.provide(Layer.merge(documentAccess, seo)));
|
|
4630
|
+
return Layer.mergeAll(activity, documentAccess, workingDocuments, seo, seoDelivery, readinessLayer);
|
|
4631
|
+
}
|
|
4632
|
+
function productModulesWithMediaLayer(options) {
|
|
4633
|
+
const products = productModulesLayer({
|
|
4634
|
+
...options,
|
|
4635
|
+
documentEngine: {
|
|
4636
|
+
...options.documentEngine,
|
|
4637
|
+
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? options.mediaPublicBaseUrl
|
|
4638
|
+
}
|
|
5728
4639
|
});
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
4640
|
+
const media = mediaLibraryLayer({
|
|
4641
|
+
maxUploadBytes: options.bootstrap.media.maxUploadBytes,
|
|
4642
|
+
publicBaseUrl: options.mediaPublicBaseUrl
|
|
4643
|
+
}).pipe(Layer.provide(products));
|
|
4644
|
+
return Layer.merge(products, media);
|
|
5732
4645
|
}
|
|
5733
4646
|
|
|
5734
4647
|
//#endregion
|
|
@@ -5831,20 +4744,21 @@ const reconcileStored = Effect.fn("Initialization.reconcileStored")(function* (s
|
|
|
5831
4744
|
id: state.claim.changeId
|
|
5832
4745
|
}, state.claim.intent);
|
|
5833
4746
|
const begin = yield* journal.begin(draft).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
case "Unresolved": {
|
|
5842
|
-
const resolution = yield* reconcileInitialAdministrator(journal, begin.change, recovery.observe).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
|
|
5843
|
-
if (resolution.kind === "applied") yield* store.complete(state.claim.changeId);
|
|
5844
|
-
if (resolution.kind === "cancelled") yield* store.reset(state.claim.changeId);
|
|
5845
|
-
return yield* store.read;
|
|
5846
|
-
}
|
|
4747
|
+
if (Predicate.isTagged("Recorded")(begin)) {
|
|
4748
|
+
yield* store.complete(state.claim.changeId);
|
|
4749
|
+
return yield* store.read;
|
|
4750
|
+
}
|
|
4751
|
+
if (Predicate.isTagged("Cancelled")(begin)) {
|
|
4752
|
+
yield* store.reset(state.claim.changeId);
|
|
4753
|
+
return yield* store.read;
|
|
5847
4754
|
}
|
|
4755
|
+
if (Predicate.isTagged("Unresolved")(begin)) {
|
|
4756
|
+
const resolution = yield* reconcileInitialAdministrator(journal, begin.change, recovery.observe).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
|
|
4757
|
+
if (resolution.kind === "applied") yield* store.complete(state.claim.changeId);
|
|
4758
|
+
if (resolution.kind === "cancelled") yield* store.reset(state.claim.changeId);
|
|
4759
|
+
return yield* store.read;
|
|
4760
|
+
}
|
|
4761
|
+
return yield* store.read;
|
|
5848
4762
|
});
|
|
5849
4763
|
function completeBestEffort(store, changeId) {
|
|
5850
4764
|
return store.complete(changeId).pipe(Effect.catch((error) => Effect.logWarning("Initialization completion remains recoverable.", {
|
|
@@ -6045,6 +4959,69 @@ function targetError(operation) {
|
|
|
6045
4959
|
return (error) => Schema.is(UserAlreadyExists)(error) ? unavailable$2(operation)(error) : error;
|
|
6046
4960
|
}
|
|
6047
4961
|
|
|
4962
|
+
//#endregion
|
|
4963
|
+
//#region ../server/src/Forms.ts
|
|
4964
|
+
/** Only the currently published definition can authorize a submission. Never trust client rules. */
|
|
4965
|
+
const submitForm = Effect.fn("Forms.submit")(function* (input, components, handlers) {
|
|
4966
|
+
const document = yield* (yield* PublishedDocuments).read({
|
|
4967
|
+
kind: "id",
|
|
4968
|
+
documentId: input.documentId
|
|
4969
|
+
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
4970
|
+
if (!document) return FormResult.make({
|
|
4971
|
+
status: "unavailable",
|
|
4972
|
+
errors: {}
|
|
4973
|
+
});
|
|
4974
|
+
const form = documentForms(document, components).find((candidate) => JSON.stringify(candidate.nodePath) === JSON.stringify(input.nodePath));
|
|
4975
|
+
if (!form || form.revision !== input.revision) return FormResult.make({
|
|
4976
|
+
status: "stale",
|
|
4977
|
+
errors: {}
|
|
4978
|
+
});
|
|
4979
|
+
const handler = Object.hasOwn(handlers, form.name) ? handlers[form.name] : void 0;
|
|
4980
|
+
if (!handler) return FormResult.make({
|
|
4981
|
+
status: "unavailable",
|
|
4982
|
+
errors: {}
|
|
4983
|
+
});
|
|
4984
|
+
const fields = form.steps.flatMap((step) => step.fields);
|
|
4985
|
+
const errors = validateFormValues(fields, input.values);
|
|
4986
|
+
if (Object.keys(input.values).some((id) => !fields.some((field) => field.id === id))) errors._form = "The submission contains unknown answers.";
|
|
4987
|
+
if (Object.keys(errors).length) return FormResult.make({
|
|
4988
|
+
status: "invalid",
|
|
4989
|
+
errors
|
|
4990
|
+
});
|
|
4991
|
+
const values = Object.fromEntries(fields.map((field) => [field.id, normalizedAnswer(Object.hasOwn(input.values, field.id) ? input.values[field.id] : void 0)]));
|
|
4992
|
+
return yield* Effect.tryPromise(() => Promise.resolve(handler({
|
|
4993
|
+
form,
|
|
4994
|
+
values
|
|
4995
|
+
}))).pipe(Effect.as(FormResult.make({
|
|
4996
|
+
status: "accepted",
|
|
4997
|
+
errors: {}
|
|
4998
|
+
})), Effect.orElseSucceed(() => FormResult.make({
|
|
4999
|
+
status: "failed",
|
|
5000
|
+
errors: {}
|
|
5001
|
+
})));
|
|
5002
|
+
});
|
|
5003
|
+
function normalizedAnswer(value) {
|
|
5004
|
+
return value === void 0 || Predicate.isString(value) && !value.trim() ? null : value;
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
//#endregion
|
|
5008
|
+
//#region ../platform-node/src/internal/http/forms.ts
|
|
5009
|
+
function formsApiLayer(components, handlers) {
|
|
5010
|
+
return HttpApiBuilder.group(StudioApi, "forms", (group) => Effect.gen(function* () {
|
|
5011
|
+
const documents = yield* PublishedDocuments;
|
|
5012
|
+
const limiter = yield* RateLimiter.RateLimiter;
|
|
5013
|
+
return group.handle("submit", ({ payload }) => limiter.consume({
|
|
5014
|
+
key: "cms-forms",
|
|
5015
|
+
limit: 60,
|
|
5016
|
+
window: "1 minute",
|
|
5017
|
+
onExceeded: "fail"
|
|
5018
|
+
}).pipe(Effect.andThen(submitForm(payload, components, handlers).pipe(Effect.provideService(PublishedDocuments, documents))), Effect.orElseSucceed(() => FormResult.make({
|
|
5019
|
+
status: "unavailable",
|
|
5020
|
+
errors: {}
|
|
5021
|
+
}))));
|
|
5022
|
+
})).pipe(Layer.provide(RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory))));
|
|
5023
|
+
}
|
|
5024
|
+
|
|
6048
5025
|
//#endregion
|
|
6049
5026
|
//#region ../platform-node/src/internal/http/errors.ts
|
|
6050
5027
|
const studioSchemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(StudioSchemaError, (error) => Effect.fail(mapStudioSchemaError(error.kind)));
|
|
@@ -6065,7 +5042,7 @@ function mapUnexpectedError(effect) {
|
|
|
6065
5042
|
//#region ../platform-node/src/internal/http/collaboration.ts
|
|
6066
5043
|
const PublicTicketError = Schema.Union([AccessDenied, DocumentNotFound]);
|
|
6067
5044
|
const isPublicTicketError = Schema.is(PublicTicketError);
|
|
6068
|
-
const collaborationApiLayer = HttpApiBuilder.group(StudioApi, "collaboration", (handlers) => Effect.gen(function* () {
|
|
5045
|
+
const collaborationApiLayer = (authoringRevision) => HttpApiBuilder.group(StudioApi, "collaboration", (handlers) => Effect.gen(function* () {
|
|
6069
5046
|
const accessControl = yield* AccessControl;
|
|
6070
5047
|
const documents = yield* WorkingDocuments;
|
|
6071
5048
|
const tickets = yield* StudioRealtimeTickets;
|
|
@@ -6090,6 +5067,7 @@ const collaborationApiLayer = HttpApiBuilder.group(StudioApi, "collaboration", (
|
|
|
6090
5067
|
ref: opened.ref
|
|
6091
5068
|
});
|
|
6092
5069
|
return CollaborationTicket.make({
|
|
5070
|
+
authoringRevision,
|
|
6093
5071
|
authoredStateVector: encodeRealtimeStateVector(opened.authoredStateVector),
|
|
6094
5072
|
documentName: roomName,
|
|
6095
5073
|
source: opened.source,
|
|
@@ -6124,7 +5102,13 @@ const documentsApiLayer = HttpApiBuilder.group(StudioApi, "documents", (handlers
|
|
|
6124
5102
|
}))).pipe(mapMutationError)).handle("validate", ({ params, payload }) => withStudioRequest((context) => documents.validate(context, UpdateDocumentInput.make({
|
|
6125
5103
|
documentId: params.documentId,
|
|
6126
5104
|
...payload
|
|
6127
|
-
}))).pipe(preservePublicErrors((error) => isAccessDenied$1(error) || isDocumentNotFound$1(error) || isDocumentVersionConflict(error)))).handle("
|
|
5105
|
+
}))).pipe(preservePublicErrors((error) => isAccessDenied$1(error) || isDocumentNotFound$1(error) || isDocumentVersionConflict(error)))).handle("duplicate", ({ params, payload }) => withStudioRequest((context) => documents.duplicate(context, DuplicateDocumentInput.make({
|
|
5106
|
+
documentId: params.documentId,
|
|
5107
|
+
...payload
|
|
5108
|
+
}))).pipe(mapMutationError)).handle("unpublish", ({ params, payload }) => withStudioRequest((context) => documents.unpublish(context, UnpublishDocumentInput.make({
|
|
5109
|
+
documentId: params.documentId,
|
|
5110
|
+
...payload
|
|
5111
|
+
}))).pipe(mapMutationError)).handle("move", ({ params, payload }) => withStudioRequest((context) => documents.move(context, MoveDocumentInput.make({
|
|
6128
5112
|
documentId: params.documentId,
|
|
6129
5113
|
...payload
|
|
6130
5114
|
}))).pipe(mapMutationError)).handle("publish", ({ params, payload }) => withStudioRequest((context) => documents.publish(context, PublishDocumentInput.make({
|
|
@@ -6219,7 +5203,7 @@ function decodeMediaUpload(payload, query, maxUploadBytes) {
|
|
|
6219
5203
|
}
|
|
6220
5204
|
function mapMultipartError(error) {
|
|
6221
5205
|
if (isMediaInvalid(error)) return error;
|
|
6222
|
-
return
|
|
5206
|
+
return Predicate.isTagged("InternalError")(error.reason) ? studioInternalError() : invalidMultipart();
|
|
6223
5207
|
}
|
|
6224
5208
|
function invalidMultipart() {
|
|
6225
5209
|
return new MediaInvalid({ message: "Upload exactly one file whose body matches its declared size." });
|
|
@@ -6227,43 +5211,20 @@ function invalidMultipart() {
|
|
|
6227
5211
|
|
|
6228
5212
|
//#endregion
|
|
6229
5213
|
//#region ../platform-node/src/internal/http/preview.ts
|
|
6230
|
-
const
|
|
6231
|
-
const
|
|
6232
|
-
|
|
6233
|
-
const
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
}))
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
const verify = (token) => requestOrigin.pipe(Effect.flatMap((origin) => previewSessions.verify(token, origin)));
|
|
6245
|
-
return handlers.handle("resolve", ({ payload }) => Effect.gen(function* () {
|
|
6246
|
-
yield* verify(payload.token);
|
|
6247
|
-
const document = yield* documents.read(payload.lookup);
|
|
6248
|
-
return Option.isSome(document) ? document.value : yield* new PreviewDraftNotFound({ lookup: payload.lookup });
|
|
6249
|
-
}).pipe(preservePublicErrors(isPreviewResolvePublicError))).handle("invalidations", ({ payload }) => Effect.gen(function* () {
|
|
6250
|
-
const claims = yield* verify(payload.token);
|
|
6251
|
-
const now = yield* Clock.currentTimeMillis;
|
|
6252
|
-
const expiresIn = Math.max(0, claims.expiresAt - now);
|
|
6253
|
-
return generationInvalidations(documents, options.invalidationInterval ?? "1 second").pipe(Stream.interruptWhen(Effect.sleep(Duration.millis(expiresIn))));
|
|
6254
|
-
}).pipe(preservePublicErrors(isPreviewSessionInvalid)));
|
|
6255
|
-
}));
|
|
6256
|
-
return Layer.merge(sessions, delivery);
|
|
6257
|
-
}
|
|
6258
|
-
function generationInvalidations(documents, interval) {
|
|
6259
|
-
return Stream.make(0).pipe(Stream.concat(Stream.fromSchedule(Schedule.spaced(interval))), Stream.mapEffect(() => documents.generation), Stream.changes, Stream.drop(1), Stream.map((generation) => PreviewInvalidation.make({
|
|
6260
|
-
generation,
|
|
6261
|
-
type: "document-invalidated"
|
|
6262
|
-
})), Stream.catch(() => Stream.empty));
|
|
6263
|
-
}
|
|
6264
|
-
const requestOrigin = HttpServerRequest.HttpServerRequest.pipe(Effect.flatMap((request) => {
|
|
6265
|
-
const origin = request.headers.origin;
|
|
6266
|
-
return origin === void 0 ? Effect.fail(new PreviewSessionInvalid({ reason: "origin" })) : Schema.decodeEffect(PreviewOrigin)(origin).pipe(Effect.mapError(() => new PreviewSessionInvalid({ reason: "origin" })));
|
|
5214
|
+
const publicError = Schema.is(Schema.Union([AccessDenied, PublishedDocumentNotFound]));
|
|
5215
|
+
const denied = Schema.is(AccessDenied);
|
|
5216
|
+
const previewApiLayer = HttpApiBuilder.group(StudioApi, "previewDelivery", (handlers) => Effect.gen(function* () {
|
|
5217
|
+
const documents = yield* DocumentPreviews;
|
|
5218
|
+
const accessControl = yield* AccessControl;
|
|
5219
|
+
const seo = yield* PreviewSeo;
|
|
5220
|
+
const seoDelivery = yield* SeoDelivery;
|
|
5221
|
+
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({
|
|
5222
|
+
previous: surround.previous ? seoDelivery.previewDocument(surround.previous) : Effect.succeed(null),
|
|
5223
|
+
next: surround.next ? seoDelivery.previewDocument(surround.next) : Effect.succeed(null)
|
|
5224
|
+
})))), preservePublicErrors(publicError))).handle("seo", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => accessControl.require(context, {
|
|
5225
|
+
action: "document.browse",
|
|
5226
|
+
resource: { kind: "catalog" }
|
|
5227
|
+
}).pipe(Effect.andThen(seo.read))), preservePublicErrors(denied)));
|
|
6267
5228
|
}));
|
|
6268
5229
|
|
|
6269
5230
|
//#endregion
|
|
@@ -6271,7 +5232,15 @@ const requestOrigin = HttpServerRequest.HttpServerRequest.pipe(Effect.flatMap((r
|
|
|
6271
5232
|
const isPublishedDocumentNotFound = Schema.is(PublishedDocumentNotFound);
|
|
6272
5233
|
const deliveryApiLayer = Layer.merge(HttpApiBuilder.group(StudioApi, "delivery", (handlers) => Effect.gen(function* () {
|
|
6273
5234
|
const documents = yield* PublishedDocuments;
|
|
6274
|
-
|
|
5235
|
+
const seo = yield* PublishedSeo;
|
|
5236
|
+
const seoDelivery = yield* SeoDelivery;
|
|
5237
|
+
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) => ({
|
|
5238
|
+
items,
|
|
5239
|
+
total: page.total
|
|
5240
|
+
})))), mapUnexpectedError)).handle("navigation", ({ payload }) => documents.navigation(payload).pipe(mapUnexpectedError)).handle("surround", ({ payload }) => documents.surround(payload.lookup, payload.query).pipe(Effect.flatMap((surround) => Effect.all({
|
|
5241
|
+
previous: surround.previous ? seoDelivery.publishedDocument(surround.previous) : Effect.succeed(null),
|
|
5242
|
+
next: surround.next ? seoDelivery.publishedDocument(surround.next) : Effect.succeed(null)
|
|
5243
|
+
})), preservePublicErrors(isPublishedDocumentNotFound))).handle("seo", () => seo.read.pipe(mapUnexpectedError)).handle("sitemap", () => seoDelivery.sitemap.pipe(mapUnexpectedError));
|
|
6275
5244
|
})), HttpApiBuilder.group(StudioApi, "diagnostics", (handlers) => Effect.gen(function* () {
|
|
6276
5245
|
const documents = yield* PublishedDocuments;
|
|
6277
5246
|
return handlers.handle("documents", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.diagnostics(context)), preservePublicErrors(Schema.is(AccessDenied))));
|
|
@@ -6287,6 +5256,12 @@ const isHistoryPublicError = Schema.is(Schema.Union([
|
|
|
6287
5256
|
DocumentNotFound
|
|
6288
5257
|
]));
|
|
6289
5258
|
const isInitializationPublicError = Schema.is(Schema.Union([InitializationAlreadyCompleted, InitializationProofInvalid]));
|
|
5259
|
+
const isSeoPublicError = Schema.is(Schema.Union([
|
|
5260
|
+
AccessDenied,
|
|
5261
|
+
SeoConfirmationRequired,
|
|
5262
|
+
SeoMediaUnavailable,
|
|
5263
|
+
SeoVersionConflict
|
|
5264
|
+
]));
|
|
6290
5265
|
const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "initialization", (handlers) => Effect.gen(function* () {
|
|
6291
5266
|
const initialization = yield* Initialization;
|
|
6292
5267
|
return handlers.handle("status", () => initialization.status.pipe(mapUnexpectedError)).handle("claim", ({ payload }) => initialization.claim(payload).pipe(preservePublicErrors(isInitializationPublicError)));
|
|
@@ -6296,6 +5271,9 @@ const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "i
|
|
|
6296
5271
|
})), HttpApiBuilder.group(StudioApi, "activity", (handlers) => Effect.gen(function* () {
|
|
6297
5272
|
const activity = yield* Activity;
|
|
6298
5273
|
return handlers.handle("list", ({ query }) => withStudioRequest((context) => activity.list(context, query)).pipe(preservePublicErrors(isActivityPublicError)));
|
|
5274
|
+
})), HttpApiBuilder.group(StudioApi, "seo", (handlers) => Effect.gen(function* () {
|
|
5275
|
+
const seo = yield* Seo;
|
|
5276
|
+
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)));
|
|
6299
5277
|
})));
|
|
6300
5278
|
|
|
6301
5279
|
//#endregion
|
|
@@ -6321,43 +5299,14 @@ const userAdministrationHttpLayer = HttpApiBuilder.group(StudioApi, "users", (ha
|
|
|
6321
5299
|
|
|
6322
5300
|
//#endregion
|
|
6323
5301
|
//#region ../platform-node/src/internal/http/api.ts
|
|
6324
|
-
function studioApiLayer(bootstrap) {
|
|
5302
|
+
function studioApiLayer(bootstrap, authoringRevision, components = [], formHandlers = {}) {
|
|
6325
5303
|
const configuration = HttpApiBuilder.group(StudioApi, "configuration", (handlers) => handlers.handle("bootstrap", () => Effect.succeed(bootstrap)));
|
|
6326
|
-
return authenticatedRouterHandlers(Layer.mergeAll(configuration, documentsApiLayer, mediaApiLayer(bootstrap.media.enabled, bootstrap.media.maxUploadBytes), collaborationApiLayer, deliveryApiLayer,
|
|
5304
|
+
return authenticatedRouterHandlers(Layer.mergeAll(configuration, formsApiLayer(components, formHandlers), documentsApiLayer, mediaApiLayer(bootstrap.media.enabled, bootstrap.media.maxUploadBytes), collaborationApiLayer(authoringRevision), deliveryApiLayer, previewApiLayer, administrationApiLayer, userAdministrationHttpLayer).pipe(Layer.provideMerge(studioSchemaErrorLayer)));
|
|
6327
5305
|
}
|
|
6328
5306
|
function authenticatedRouterHandlers(handlers) {
|
|
6329
5307
|
return handlers;
|
|
6330
5308
|
}
|
|
6331
5309
|
|
|
6332
|
-
//#endregion
|
|
6333
|
-
//#region ../platform-node/src/internal/installation/server-modules.ts
|
|
6334
|
-
function productModulesLayer(options) {
|
|
6335
|
-
const accessControl = defaultAccessControlLayer;
|
|
6336
|
-
const authoredDocuments = documentsLayer(options.bootstrap.documents, {
|
|
6337
|
-
...options.documentEngine,
|
|
6338
|
-
components: options.documentEngine.components ?? options.bootstrap.components,
|
|
6339
|
-
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? "https://media.invalid"
|
|
6340
|
-
}).pipe(Layer.provide(accessControl));
|
|
6341
|
-
const documentAccess = Layer.merge(authoredDocuments, accessControl);
|
|
6342
|
-
const activity = activityLayer.pipe(Layer.provide(accessControl));
|
|
6343
|
-
const workingDocuments = workingDocumentsLayer(options.workingDocuments).pipe(Layer.provide(documentAccess));
|
|
6344
|
-
return Layer.mergeAll(activity, documentAccess, workingDocuments);
|
|
6345
|
-
}
|
|
6346
|
-
function productModulesWithMediaLayer(options) {
|
|
6347
|
-
const products = productModulesLayer({
|
|
6348
|
-
...options,
|
|
6349
|
-
documentEngine: {
|
|
6350
|
-
...options.documentEngine,
|
|
6351
|
-
mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? options.mediaPublicBaseUrl
|
|
6352
|
-
}
|
|
6353
|
-
});
|
|
6354
|
-
const media = mediaLibraryLayer({
|
|
6355
|
-
maxUploadBytes: options.bootstrap.media.maxUploadBytes,
|
|
6356
|
-
publicBaseUrl: options.mediaPublicBaseUrl
|
|
6357
|
-
}).pipe(Layer.provide(products));
|
|
6358
|
-
return Layer.merge(products, media);
|
|
6359
|
-
}
|
|
6360
|
-
|
|
6361
5310
|
//#endregion
|
|
6362
5311
|
//#region ../platform-node/src/internal/installation/error.ts
|
|
6363
5312
|
var CmsInstallationError = class extends Schema.TaggedError()("Cms.InstallationError", {
|
|
@@ -6374,8 +5323,12 @@ function makeInstallationLayer(validated) {
|
|
|
6374
5323
|
const options = validated.options;
|
|
6375
5324
|
const core = options.composition.core;
|
|
6376
5325
|
const productOptions = {
|
|
5326
|
+
baseUrl: options.baseUrl,
|
|
6377
5327
|
bootstrap: options.bootstrap,
|
|
6378
|
-
documentEngine: {
|
|
5328
|
+
documentEngine: {
|
|
5329
|
+
documentModel: options.documentModel,
|
|
5330
|
+
components: options.authoring.components
|
|
5331
|
+
},
|
|
6379
5332
|
workingDocuments: options.workingDocuments
|
|
6380
5333
|
};
|
|
6381
5334
|
const enabledProductModules = (media) => productModulesWithMediaLayer({
|
|
@@ -6384,7 +5337,6 @@ function makeInstallationLayer(validated) {
|
|
|
6384
5337
|
}).pipe(Layer.provide(Layer.merge(core, media.objects)));
|
|
6385
5338
|
const productModules = validated.media.enabled ? enabledProductModules(validated.media.configuration) : productModulesLayer(productOptions).pipe(Layer.provide(core));
|
|
6386
5339
|
const authentication = studioAuthenticationLayer.pipe(Layer.provide(core));
|
|
6387
|
-
const previewSessions = previewSessionsLayer(options.studioSecret, { allowedPreviewOrigins: options.allowedPreviewOrigins }).pipe(Layer.provide(core));
|
|
6388
5340
|
const initialization = initializationLayer({
|
|
6389
5341
|
initialSources: options.initialSources,
|
|
6390
5342
|
initializationToken: options.initializationToken
|
|
@@ -6393,10 +5345,10 @@ function makeInstallationLayer(validated) {
|
|
|
6393
5345
|
const tickets = studioRealtimeTicketsLayer(options.studioSecret).pipe(Layer.provide(core));
|
|
6394
5346
|
const roomHost = studioRealtimeRoomHostLayer().pipe(Layer.provide(productModules));
|
|
6395
5347
|
const gateway = studioRealtimeGatewayLayer.pipe(Layer.provide(Layer.mergeAll(productModules, roomHost, tickets)));
|
|
6396
|
-
const httpDependencies = Layer.mergeAll(core, productModules, gateway, initialization,
|
|
6397
|
-
const handlers = studioApiLayer(options.bootstrap).pipe(Layer.provide(httpDependencies));
|
|
6398
|
-
const httpApi = HttpApiBuilder.layer(StudioApi).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices));
|
|
6399
|
-
return validated.media.enabled ? installationHttpLayer(options.bootstrap,
|
|
5348
|
+
const httpDependencies = Layer.mergeAll(core, productModules, gateway, initialization, roomHost, users).pipe(Layer.provideMerge(authentication), Layer.provideMerge(tickets));
|
|
5349
|
+
const handlers = studioApiLayer(options.bootstrap, options.authoring.revision, options.authoring.components, options.formHandlers).pipe(Layer.provide(httpDependencies));
|
|
5350
|
+
const httpApi = HttpApiBuilder.layer(StudioApi).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer));
|
|
5351
|
+
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)));
|
|
6400
5352
|
}
|
|
6401
5353
|
function validateInstallationOptions(options) {
|
|
6402
5354
|
if (options.bootstrap.media.enabled) {
|
|
@@ -6434,7 +5386,6 @@ function mapStartupError(layer, operation) {
|
|
|
6434
5386
|
|
|
6435
5387
|
//#endregion
|
|
6436
5388
|
//#region ../platform-node/src/Installation.ts
|
|
6437
|
-
/** Starts auth, persistence, Studio realtime, delivery, health, and maintenance as one deep module. */
|
|
6438
5389
|
const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
|
|
6439
5390
|
return yield* startInstallation({
|
|
6440
5391
|
cleanup: options.bootstrap.media.enabled ? options.cleanup : void 0,
|
|
@@ -6443,7 +5394,9 @@ const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
|
|
|
6443
5394
|
});
|
|
6444
5395
|
function createCmsInstallationLayer(options) {
|
|
6445
5396
|
return installationLayer({
|
|
6446
|
-
|
|
5397
|
+
baseUrl: options.baseUrl,
|
|
5398
|
+
formHandlers: options.formHandlers,
|
|
5399
|
+
authoring: options.authoring,
|
|
6447
5400
|
bootstrap: options.bootstrap,
|
|
6448
5401
|
composition: options.composition,
|
|
6449
5402
|
studioSecret: options.studioSecret,
|
|
@@ -6453,262 +5406,6 @@ function createCmsInstallationLayer(options) {
|
|
|
6453
5406
|
});
|
|
6454
5407
|
}
|
|
6455
5408
|
|
|
6456
|
-
//#endregion
|
|
6457
|
-
//#region ../s3/src/internal/client.ts
|
|
6458
|
-
var S3RequestError = class extends Schema.TaggedError()("S3.RequestError", {
|
|
6459
|
-
cause: Schema.Defect(),
|
|
6460
|
-
operation: Schema.NonEmptyString
|
|
6461
|
-
}) {};
|
|
6462
|
-
function openAwsS3MediaObjectClient(options) {
|
|
6463
|
-
const credentials = options.credentials ? {
|
|
6464
|
-
accessKeyId: options.credentials.accessKeyId,
|
|
6465
|
-
secretAccessKey: Redacted.value(options.credentials.secretAccessKey),
|
|
6466
|
-
sessionToken: options.credentials.sessionToken ? Redacted.value(options.credentials.sessionToken) : void 0
|
|
6467
|
-
} : void 0;
|
|
6468
|
-
return makeAwsS3MediaObjectClient(new S3Client({
|
|
6469
|
-
credentials,
|
|
6470
|
-
endpoint: options.endpoint,
|
|
6471
|
-
forcePathStyle: options.forcePathStyle,
|
|
6472
|
-
region: options.region
|
|
6473
|
-
}));
|
|
6474
|
-
}
|
|
6475
|
-
/** Wraps the official client; exported only through the package-private path for transport-level tests. */
|
|
6476
|
-
function makeAwsS3MediaObjectClient(client) {
|
|
6477
|
-
const request = (operation, send) => Effect.tryPromise({
|
|
6478
|
-
catch: (cause) => new S3RequestError({
|
|
6479
|
-
cause,
|
|
6480
|
-
operation
|
|
6481
|
-
}),
|
|
6482
|
-
try: send
|
|
6483
|
-
});
|
|
6484
|
-
return {
|
|
6485
|
-
close: Effect.sync(() => client.destroy()),
|
|
6486
|
-
copyObject: (input) => request("MediaObjectStore.S3.copyObject", (signal) => client.send(new CopyObjectCommand({
|
|
6487
|
-
Bucket: input.bucket,
|
|
6488
|
-
CacheControl: input.cacheControl,
|
|
6489
|
-
ContentType: input.contentType,
|
|
6490
|
-
CopySource: input.copySource,
|
|
6491
|
-
Key: input.key,
|
|
6492
|
-
MetadataDirective: "REPLACE"
|
|
6493
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6494
|
-
createBucket: (input) => request("MediaObjectStore.S3.createBucket", (signal) => client.send(new CreateBucketCommand({
|
|
6495
|
-
Bucket: input.bucket,
|
|
6496
|
-
CreateBucketConfiguration: input.locationConstraint ? { LocationConstraint: input.locationConstraint } : void 0
|
|
6497
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6498
|
-
deleteObject: (input) => request("MediaObjectStore.S3.deleteObject", (signal) => client.send(new DeleteObjectCommand({
|
|
6499
|
-
Bucket: input.bucket,
|
|
6500
|
-
Key: input.key
|
|
6501
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6502
|
-
headBucket: (input) => request("MediaObjectStore.S3.headBucket", (signal) => client.send(new HeadBucketCommand({ Bucket: input.bucket }), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
6503
|
-
headObject: (input) => request("MediaObjectStore.S3.headObject", (signal) => client.send(new HeadObjectCommand({
|
|
6504
|
-
Bucket: input.bucket,
|
|
6505
|
-
Key: input.key
|
|
6506
|
-
}), { abortSignal: signal })).pipe(Effect.map((output) => ({
|
|
6507
|
-
contentLength: output.ContentLength,
|
|
6508
|
-
contentType: output.ContentType
|
|
6509
|
-
}))),
|
|
6510
|
-
listObjects: (input) => request("MediaObjectStore.S3.listObjects", (signal) => client.send(new ListObjectsV2Command({
|
|
6511
|
-
Bucket: input.bucket,
|
|
6512
|
-
ContinuationToken: input.continuationToken,
|
|
6513
|
-
Prefix: input.prefix
|
|
6514
|
-
}), { abortSignal: signal })).pipe(Effect.map((output) => ({
|
|
6515
|
-
isTruncated: output.IsTruncated === true,
|
|
6516
|
-
nextContinuationToken: output.NextContinuationToken,
|
|
6517
|
-
objects: (output.Contents ?? []).map((object) => ({
|
|
6518
|
-
key: object.Key,
|
|
6519
|
-
size: object.Size
|
|
6520
|
-
}))
|
|
6521
|
-
}))),
|
|
6522
|
-
putObject: (input) => request("MediaObjectStore.S3.putObject", (signal) => client.send(new PutObjectCommand({
|
|
6523
|
-
Body: Readable.from(input.body),
|
|
6524
|
-
Bucket: input.bucket,
|
|
6525
|
-
CacheControl: input.cacheControl,
|
|
6526
|
-
ContentLength: input.contentLength,
|
|
6527
|
-
ContentType: input.contentType,
|
|
6528
|
-
Key: input.key
|
|
6529
|
-
}), { abortSignal: signal })).pipe(Effect.asVoid)
|
|
6530
|
-
};
|
|
6531
|
-
}
|
|
6532
|
-
|
|
6533
|
-
//#endregion
|
|
6534
|
-
//#region ../s3/src/internal/media-object-store.ts
|
|
6535
|
-
const mediaPrefix = "media/";
|
|
6536
|
-
const replaceableCacheControl = "public, max-age=0, must-revalidate";
|
|
6537
|
-
const isWriteBodyError = Schema.is(Schema.Union([MediaInvalid, MediaStorageError]));
|
|
6538
|
-
/** Owns and closes the official AWS client for the lifetime of the layer. */
|
|
6539
|
-
function s3MediaObjectStoreLayer(options) {
|
|
6540
|
-
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))))));
|
|
6541
|
-
}
|
|
6542
|
-
function makeStore(options, client) {
|
|
6543
|
-
return {
|
|
6544
|
-
list: listObjects(options.bucket, client),
|
|
6545
|
-
promote: (input) => promoteObject(options.bucket, input, client),
|
|
6546
|
-
put: (input) => putObject(options.bucket, input, client),
|
|
6547
|
-
remove: (objectKey) => providerRequest("MediaObjectStore.S3.remove", client.deleteObject({
|
|
6548
|
-
bucket: options.bucket,
|
|
6549
|
-
key: objectKey
|
|
6550
|
-
})),
|
|
6551
|
-
stat: (objectKey) => statObject(options.bucket, objectKey, client)
|
|
6552
|
-
};
|
|
6553
|
-
}
|
|
6554
|
-
function promoteObject(bucket, input, client) {
|
|
6555
|
-
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({
|
|
6556
|
-
bucket,
|
|
6557
|
-
cacheControl: replaceableCacheControl,
|
|
6558
|
-
contentType: input.mimeType,
|
|
6559
|
-
copySource: copySource(bucket, input.sourceObjectKey),
|
|
6560
|
-
key: input.destinationObjectKey
|
|
6561
|
-
}))), 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."))));
|
|
6562
|
-
}
|
|
6563
|
-
function copySource(bucket, objectKey) {
|
|
6564
|
-
return `/${encodeURIComponent(bucket)}/${objectKey.split("/").map(encodeURIComponent).join("/")}`;
|
|
6565
|
-
}
|
|
6566
|
-
function putObject(bucket, input, client) {
|
|
6567
|
-
return Effect.gen(function* () {
|
|
6568
|
-
const body = yield* validatedWriteBody(input);
|
|
6569
|
-
const iterable = yield* Stream.toAsyncIterableEffect(body);
|
|
6570
|
-
yield* client.putObject({
|
|
6571
|
-
body: iterable,
|
|
6572
|
-
bucket,
|
|
6573
|
-
cacheControl: replaceableCacheControl,
|
|
6574
|
-
contentLength: input.byteSize,
|
|
6575
|
-
contentType: input.mimeType,
|
|
6576
|
-
key: input.objectKey
|
|
6577
|
-
}).pipe(Effect.catch((failure) => isWriteBodyError(failure.cause) ? Effect.fail(failure.cause) : Effect.fail(providerError("MediaObjectStore.S3.put", failure))));
|
|
6578
|
-
return {
|
|
6579
|
-
byteSize: input.byteSize,
|
|
6580
|
-
mimeType: input.mimeType,
|
|
6581
|
-
objectKey: input.objectKey
|
|
6582
|
-
};
|
|
6583
|
-
});
|
|
6584
|
-
}
|
|
6585
|
-
const validatedWriteBody = Effect.fn("MediaObjectStore.S3.validateWriteBody")(function* (input) {
|
|
6586
|
-
const byteLength = yield* Ref.make(0);
|
|
6587
|
-
return input.body.pipe(Stream.mapEffect((chunk) => Effect.gen(function* () {
|
|
6588
|
-
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.");
|
|
6589
|
-
return chunk;
|
|
6590
|
-
})), 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)));
|
|
6591
|
-
});
|
|
6592
|
-
function statObject(bucket, objectKey, client) {
|
|
6593
|
-
return client.headObject({
|
|
6594
|
-
bucket,
|
|
6595
|
-
key: objectKey
|
|
6596
|
-
}).pipe(Effect.matchEffect({
|
|
6597
|
-
onFailure: (failure) => isMissingObject(failure.cause) ? Effect.succeed(Option.none()) : Effect.fail(providerError("MediaObjectStore.S3.stat", failure)),
|
|
6598
|
-
onSuccess: (output) => decodeStoredObject("MediaObjectStore.S3.stat", objectKey, output.contentLength, output.contentType).pipe(Effect.asSome)
|
|
6599
|
-
}));
|
|
6600
|
-
}
|
|
6601
|
-
function listObjects(bucket, client) {
|
|
6602
|
-
return Stream.paginate({ seenTokens: /* @__PURE__ */ new Set() }, (state) => providerRequest("MediaObjectStore.S3.list", client.listObjects({
|
|
6603
|
-
bucket,
|
|
6604
|
-
continuationToken: state.continuationToken,
|
|
6605
|
-
prefix: mediaPrefix
|
|
6606
|
-
})).pipe(Effect.flatMap((page) => Effect.gen(function* () {
|
|
6607
|
-
return [yield* Effect.forEach(page.objects, decodeListedObject), yield* nextListState(state, page)];
|
|
6608
|
-
}))));
|
|
6609
|
-
}
|
|
6610
|
-
function decodeListedObject(object) {
|
|
6611
|
-
if (object.key === void 0) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned object metadata without a key."));
|
|
6612
|
-
return decodeStoredObject("MediaObjectStore.S3.list", object.key, object.size);
|
|
6613
|
-
}
|
|
6614
|
-
function nextListState(state, page) {
|
|
6615
|
-
if (!page.isTruncated) return Effect.succeedNone;
|
|
6616
|
-
const token = page.nextContinuationToken;
|
|
6617
|
-
if (!token || token === state.continuationToken || state.seenTokens.has(token)) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned an invalid continuation token."));
|
|
6618
|
-
return Effect.succeedSome({
|
|
6619
|
-
continuationToken: token,
|
|
6620
|
-
seenTokens: /* @__PURE__ */ new Set([...state.seenTokens, token])
|
|
6621
|
-
});
|
|
6622
|
-
}
|
|
6623
|
-
function decodeStoredObject(operation, objectKey, byteSize, contentType) {
|
|
6624
|
-
const decodedObjectKey = Schema.decodeOption(MediaObjectKey)(objectKey);
|
|
6625
|
-
const decodedByteSize = Schema.decodeUnknownOption(MediaByteSize)(byteSize);
|
|
6626
|
-
if (Option.isNone(decodedObjectKey) || Option.isNone(decodedByteSize)) return Effect.fail(storageError(operation, "The provider returned invalid object metadata."));
|
|
6627
|
-
const decodedMimeType = Schema.decodeUnknownOption(ImageMimeType)(contentType);
|
|
6628
|
-
const storedObject = {
|
|
6629
|
-
byteSize: decodedByteSize.value,
|
|
6630
|
-
objectKey: decodedObjectKey.value
|
|
6631
|
-
};
|
|
6632
|
-
return Effect.succeed(Option.isSome(decodedMimeType) ? {
|
|
6633
|
-
...storedObject,
|
|
6634
|
-
mimeType: decodedMimeType.value
|
|
6635
|
-
} : storedObject);
|
|
6636
|
-
}
|
|
6637
|
-
function providerRequest(operation, request) {
|
|
6638
|
-
return request.pipe(Effect.mapError((failure) => providerError(operation, failure)));
|
|
6639
|
-
}
|
|
6640
|
-
function provisionBucket(options, client) {
|
|
6641
|
-
if (!options.provisionBucket) return Effect.void;
|
|
6642
|
-
return client.headBucket({ bucket: options.bucket }).pipe(Effect.catch((failure) => isMissingBucket(failure.cause) ? createMissingBucket(options, client) : Effect.fail(providerError("MediaObjectStore.S3.bucket.head", failure))));
|
|
6643
|
-
}
|
|
6644
|
-
function createMissingBucket(options, client) {
|
|
6645
|
-
return client.createBucket({
|
|
6646
|
-
bucket: options.bucket,
|
|
6647
|
-
locationConstraint: options.endpoint || options.region === "us-east-1" ? void 0 : options.region
|
|
6648
|
-
}).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))));
|
|
6649
|
-
}
|
|
6650
|
-
const ProviderFailureDetails = Schema.Struct({
|
|
6651
|
-
$metadata: Schema.optionalKey(Schema.Struct({
|
|
6652
|
-
attempts: Schema.optionalKey(Schema.Finite),
|
|
6653
|
-
httpStatusCode: Schema.optionalKey(Schema.Finite)
|
|
6654
|
-
})),
|
|
6655
|
-
name: Schema.optionalKey(Schema.String)
|
|
6656
|
-
});
|
|
6657
|
-
const decodeProviderFailure = Schema.decodeUnknownOption(ProviderFailureDetails);
|
|
6658
|
-
function providerError(operation, failure) {
|
|
6659
|
-
const details = decodeProviderFailure(failure.cause);
|
|
6660
|
-
const diagnostics = [];
|
|
6661
|
-
if (Option.isSome(details)) {
|
|
6662
|
-
if (details.value.name) diagnostics.push(`name=${details.value.name}`);
|
|
6663
|
-
if (details.value.$metadata?.httpStatusCode !== void 0) diagnostics.push(`status=${details.value.$metadata.httpStatusCode}`);
|
|
6664
|
-
if (details.value.$metadata?.attempts !== void 0) diagnostics.push(`attempts=${details.value.$metadata.attempts}`);
|
|
6665
|
-
}
|
|
6666
|
-
return storageError(operation, `The S3 provider request failed${diagnostics.length > 0 ? ` (${diagnostics.join(", ")}).` : "."}`);
|
|
6667
|
-
}
|
|
6668
|
-
function isMissingObject(cause) {
|
|
6669
|
-
const details = decodeProviderFailure(cause);
|
|
6670
|
-
if (Option.isNone(details)) return false;
|
|
6671
|
-
return details.value.name === "NoSuchKey" || details.value.name === "NotFound";
|
|
6672
|
-
}
|
|
6673
|
-
function isMissingBucket(cause) {
|
|
6674
|
-
const details = decodeProviderFailure(cause);
|
|
6675
|
-
if (Option.isNone(details)) return false;
|
|
6676
|
-
return details.value.name === "NoSuchBucket" || details.value.$metadata?.httpStatusCode === 404;
|
|
6677
|
-
}
|
|
6678
|
-
function isBucketAlreadyOwned(cause) {
|
|
6679
|
-
const details = decodeProviderFailure(cause);
|
|
6680
|
-
return Option.isSome(details) && details.value.name === "BucketAlreadyOwnedByYou";
|
|
6681
|
-
}
|
|
6682
|
-
function resolveOptions(options) {
|
|
6683
|
-
const bucket = options.bucket.trim();
|
|
6684
|
-
const region = options.region.trim();
|
|
6685
|
-
const endpoint = options.endpoint?.trim();
|
|
6686
|
-
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."));
|
|
6687
|
-
if (options.credentials) {
|
|
6688
|
-
const secretAccessKey = Redacted.value(options.credentials.secretAccessKey);
|
|
6689
|
-
const sessionToken = options.credentials.sessionToken && Redacted.value(options.credentials.sessionToken);
|
|
6690
|
-
if (!options.credentials.accessKeyId.trim() || !secretAccessKey || sessionToken === "") return Effect.fail(storageError("MediaObjectStore.S3.configure", "The configured credentials must be non-empty."));
|
|
6691
|
-
}
|
|
6692
|
-
return Effect.succeed({
|
|
6693
|
-
...options,
|
|
6694
|
-
bucket,
|
|
6695
|
-
credentials: options.credentials ? {
|
|
6696
|
-
...options.credentials,
|
|
6697
|
-
accessKeyId: options.credentials.accessKeyId.trim()
|
|
6698
|
-
} : void 0,
|
|
6699
|
-
endpoint,
|
|
6700
|
-
forcePathStyle: options.forcePathStyle ?? false,
|
|
6701
|
-
provisionBucket: options.provisionBucket ?? false,
|
|
6702
|
-
region
|
|
6703
|
-
});
|
|
6704
|
-
}
|
|
6705
|
-
function storageError(operation, message) {
|
|
6706
|
-
return new MediaStorageError({
|
|
6707
|
-
cause: new Error(message),
|
|
6708
|
-
operation
|
|
6709
|
-
});
|
|
6710
|
-
}
|
|
6711
|
-
|
|
6712
5409
|
//#endregion
|
|
6713
5410
|
//#region ../sql-pg/src/internal/database/table-helpers.ts
|
|
6714
5411
|
const transactionTimestamp = sql`transaction_timestamp()`;
|
|
@@ -6722,6 +5419,7 @@ const ACTIVITY_TARGET_KINDS = [
|
|
|
6722
5419
|
"document",
|
|
6723
5420
|
"media",
|
|
6724
5421
|
"media-library",
|
|
5422
|
+
"seo",
|
|
6725
5423
|
"user"
|
|
6726
5424
|
];
|
|
6727
5425
|
const IDENTIFIED_ACTIVITY_TARGET_KINDS = [
|
|
@@ -6748,8 +5446,8 @@ const activityEntries = pgTable("activity_entries", {
|
|
|
6748
5446
|
}, (table) => [
|
|
6749
5447
|
check("activity_entries_action_check", isOneOf(table.action, ACTIVITY_ACTIONS)),
|
|
6750
5448
|
check("activity_entries_target_kind_check", isOneOf(table.targetKind, ACTIVITY_TARGET_KINDS)),
|
|
6751
|
-
check("activity_entries_target_check", sql`(${table.targetKind}
|
|
6752
|
-
OR (${table.targetKind}
|
|
5449
|
+
check("activity_entries_target_check", sql`(${table.targetKind} in ('media-library', 'seo') AND ${table.targetId} IS NULL)
|
|
5450
|
+
OR (${table.targetKind} not in ('media-library', 'seo') AND ${table.targetId} IS NOT NULL)`),
|
|
6753
5451
|
check("activity_entries_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
|
|
6754
5452
|
check("activity_entries_details_check", sql`jsonb_typeof(${table.details}) = 'object'`),
|
|
6755
5453
|
index("activity_entries_history").on(table.occurredAt.desc(), table.id.desc()),
|
|
@@ -6840,27 +5538,30 @@ function activityEntryValues(entry) {
|
|
|
6840
5538
|
};
|
|
6841
5539
|
}
|
|
6842
5540
|
function activityEntriesEqual(left, right) {
|
|
6843
|
-
return isDeepStrictEqual(Schema.
|
|
5541
|
+
return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ActivityEntry)(left)), Result.getOrThrow(Schema.encodeResult(ActivityEntry)(right)));
|
|
6844
5542
|
}
|
|
6845
5543
|
function decodeActivityTarget(kind, id) {
|
|
6846
|
-
|
|
5544
|
+
if (kind === "document") return Schema.decodeUnknownEffect(ActivityTarget)({
|
|
6847
5545
|
documentId: id,
|
|
6848
5546
|
kind
|
|
6849
|
-
}
|
|
5547
|
+
});
|
|
5548
|
+
if (kind === "media") return Schema.decodeUnknownEffect(ActivityTarget)({
|
|
6850
5549
|
kind,
|
|
6851
5550
|
mediaId: id
|
|
6852
|
-
}
|
|
5551
|
+
});
|
|
5552
|
+
if (kind === "user") return Schema.decodeUnknownEffect(ActivityTarget)({
|
|
6853
5553
|
kind,
|
|
6854
5554
|
userId: id
|
|
6855
|
-
}
|
|
6856
|
-
return Schema.
|
|
5555
|
+
});
|
|
5556
|
+
return Schema.decodeEffect(ActivityTarget)({ kind });
|
|
6857
5557
|
}
|
|
6858
5558
|
function activityTargetId(entry) {
|
|
6859
5559
|
switch (entry.target.kind) {
|
|
6860
5560
|
case "document": return entry.target.documentId;
|
|
6861
5561
|
case "media": return entry.target.mediaId;
|
|
6862
5562
|
case "user": return entry.target.userId;
|
|
6863
|
-
case "media-library":
|
|
5563
|
+
case "media-library":
|
|
5564
|
+
case "seo": return;
|
|
6864
5565
|
}
|
|
6865
5566
|
}
|
|
6866
5567
|
function activityPage(entries, limit, total) {
|
|
@@ -7159,7 +5860,8 @@ function targetColumns(target) {
|
|
|
7159
5860
|
targetId: target.userId,
|
|
7160
5861
|
targetKind: target.kind
|
|
7161
5862
|
};
|
|
7162
|
-
case "media-library":
|
|
5863
|
+
case "media-library":
|
|
5864
|
+
case "seo": return { targetKind: target.kind };
|
|
7163
5865
|
}
|
|
7164
5866
|
}
|
|
7165
5867
|
function decodeOptionalTarget(kind, id) {
|
|
@@ -7167,7 +5869,7 @@ function decodeOptionalTarget(kind, id) {
|
|
|
7167
5869
|
return decodeActivityTarget(kind, id).pipe(Effect.mapError(() => new ChangeMismatch({})));
|
|
7168
5870
|
}
|
|
7169
5871
|
function draftEqual(change, draft) {
|
|
7170
|
-
return isDeepStrictEqual(Schema.
|
|
5872
|
+
return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draftFromChange(change))), Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draft)));
|
|
7171
5873
|
}
|
|
7172
5874
|
function draftFromChange(change) {
|
|
7173
5875
|
const value = {
|
|
@@ -7184,7 +5886,7 @@ function draftFromChange(change) {
|
|
|
7184
5886
|
});
|
|
7185
5887
|
}
|
|
7186
5888
|
function changeEqual(left, right) {
|
|
7187
|
-
return isDeepStrictEqual(Schema.
|
|
5889
|
+
return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(PendingChange)(left)), Result.getOrThrow(Schema.encodeResult(PendingChange)(right)));
|
|
7188
5890
|
}
|
|
7189
5891
|
function boundedLimit(limit) {
|
|
7190
5892
|
return Math.min(100, Math.max(0, Math.trunc(limit)));
|
|
@@ -7285,6 +5987,33 @@ const embeddedMigrations = [
|
|
|
7285
5987
|
folderMillis: 178875937e4,
|
|
7286
5988
|
hash: "e442485fa6c2d611e3dae508a12d245481bc5ec8f2dd4f5d853910df34484787",
|
|
7287
5989
|
name: "20260907053610_fancy_lady_deathstrike"
|
|
5990
|
+
},
|
|
5991
|
+
{
|
|
5992
|
+
sql: ["ALTER 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', 'user.created', 'user.removed', 'user.role.changed', 'user.sessions.revoked', 'authentication.signed-in', 'authentication.signed-out', 'authentication.password.changed', 'authentication.password.reset'));", "\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', 'user.created', 'user.removed', 'user.role.changed', 'user.sessions.revoked', 'authentication.signed-in', 'authentication.signed-out', 'authentication.password.changed', 'authentication.password.reset'));"],
|
|
5993
|
+
bps: true,
|
|
5994
|
+
folderMillis: 1788866265e3,
|
|
5995
|
+
hash: "24c56a37796caf8d76c0a97128a382b31e71808aa5b0ca7e5bac96795f9e16c7",
|
|
5996
|
+
name: "20260908111745_classy_johnny_storm"
|
|
5997
|
+
},
|
|
5998
|
+
{
|
|
5999
|
+
sql: [
|
|
6000
|
+
"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",
|
|
6001
|
+
"\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",
|
|
6002
|
+
"\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",
|
|
6003
|
+
"\nCREATE INDEX \"seo_media_references_media\" ON \"seo_media_references\" (\"media_id\");",
|
|
6004
|
+
"\nCREATE INDEX \"seo_revisions_history\" ON \"seo_revisions\" (\"created_at\" DESC NULLS LAST,\"id\" DESC NULLS LAST);",
|
|
6005
|
+
"\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;",
|
|
6006
|
+
"\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_working_revision_fkey\" FOREIGN KEY (\"working_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
|
|
6007
|
+
"\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_published_revision_fkey\" FOREIGN KEY (\"published_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
|
|
6008
|
+
"\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'));",
|
|
6009
|
+
"\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'));",
|
|
6010
|
+
"\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));",
|
|
6011
|
+
"\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'));"
|
|
6012
|
+
],
|
|
6013
|
+
bps: true,
|
|
6014
|
+
folderMillis: 1789233351e3,
|
|
6015
|
+
hash: "8e6a6abf5385ab3ecf8eb083c1923ac9c74d72aa71bd1d444b7402d3e7e77000",
|
|
6016
|
+
name: "20260912171551_charming_valkyrie"
|
|
7288
6017
|
}
|
|
7289
6018
|
];
|
|
7290
6019
|
|
|
@@ -7383,12 +6112,16 @@ const initializationState = pgTable("initialization_state", {
|
|
|
7383
6112
|
|
|
7384
6113
|
//#endregion
|
|
7385
6114
|
//#region ../sql-pg/src/internal/database/migrations.ts
|
|
7386
|
-
|
|
6115
|
+
const MIGRATION_LOCK_KEY = "cms:database-migrations:v1";
|
|
7387
6116
|
const migrateCmsDatabase = Effect.fn("CmsDatabase.migrate")(function* () {
|
|
6117
|
+
const postgres = yield* PgClient.PgClient;
|
|
7388
6118
|
const database = yield* PgDrizzle.makeWithDefaults();
|
|
7389
|
-
yield*
|
|
7390
|
-
|
|
7391
|
-
|
|
6119
|
+
yield* postgres.withTransaction(Effect.gen(function* () {
|
|
6120
|
+
yield* postgres.unsafe("select pg_advisory_xact_lock(hashtextextended($1, 0))", [MIGRATION_LOCK_KEY]);
|
|
6121
|
+
yield* migrate(embeddedMigrations, database._.session, "__drizzle_migrations");
|
|
6122
|
+
yield* database.insert(cmsState).values({}).onConflictDoNothing({ target: cmsState.singleton });
|
|
6123
|
+
yield* database.insert(initializationState).values({}).onConflictDoNothing({ target: initializationState.singleton });
|
|
6124
|
+
}));
|
|
7392
6125
|
});
|
|
7393
6126
|
const cmsDatabaseMigrationLayer = Layer.effectDiscard(migrateCmsDatabase());
|
|
7394
6127
|
|
|
@@ -7425,7 +6158,11 @@ const applyDocumentCommitPlan = Effect.fn("DocumentStore.Drizzle.applyCommitPlan
|
|
|
7425
6158
|
return;
|
|
7426
6159
|
}
|
|
7427
6160
|
case "document.remove": {
|
|
7428
|
-
const
|
|
6161
|
+
const metadata = {};
|
|
6162
|
+
if (plan.trashBatchId) metadata.trashBatchId = plan.trashBatchId;
|
|
6163
|
+
if (plan.title) metadata.title = plan.title;
|
|
6164
|
+
if (plan.path) metadata.path = plan.path;
|
|
6165
|
+
const revisionId = yield* appendRevision(transaction, plan, activity, metadata);
|
|
7429
6166
|
yield* transaction.update(documentsTable).set({
|
|
7430
6167
|
deletedAt: transactionTimestamp,
|
|
7431
6168
|
updatedAt: transactionTimestamp
|
|
@@ -7454,6 +6191,9 @@ const applyDocumentCommitPlan = Effect.fn("DocumentStore.Drizzle.applyCommitPlan
|
|
|
7454
6191
|
}).where(eq(documentHeads.documentId, plan.documentId));
|
|
7455
6192
|
return;
|
|
7456
6193
|
}
|
|
6194
|
+
case "document.unpublish":
|
|
6195
|
+
yield* transaction.update(documentHeads).set({ publishedRevisionId: null }).where(eq(documentHeads.documentId, plan.documentId));
|
|
6196
|
+
return;
|
|
7457
6197
|
case "document.publish":
|
|
7458
6198
|
yield* transaction.update(documentHeads).set({ publishedRevisionId: plan.revisionId }).where(eq(documentHeads.documentId, plan.documentId));
|
|
7459
6199
|
return;
|
|
@@ -7485,7 +6225,7 @@ function mapActivePathConflict(sourcePath) {
|
|
|
7485
6225
|
const causeError = Cause$1.findErrorOption(error.cause);
|
|
7486
6226
|
if (Option$1.isNone(causeError) || !SqlError.isSqlError(causeError.value)) return error;
|
|
7487
6227
|
const reason = causeError.value.reason;
|
|
7488
|
-
return
|
|
6228
|
+
return Predicate.isTagged("UniqueViolation")(reason) && reason.constraint === "documents_active_path" ? new DocumentPathConflict({ sourcePath }) : error;
|
|
7489
6229
|
}));
|
|
7490
6230
|
}
|
|
7491
6231
|
function updateAuthoredHead(transaction, documentId, revisionId) {
|
|
@@ -7505,6 +6245,7 @@ function activityActionForDocumentPlan(action) {
|
|
|
7505
6245
|
case "document.remove": return "document.trashed";
|
|
7506
6246
|
case "document.restore": return "document.revision.restored";
|
|
7507
6247
|
case "document.undelete": return "document.untrashed";
|
|
6248
|
+
case "document.unpublish": return "document.unpublished";
|
|
7508
6249
|
case "document.publish": return "document.published";
|
|
7509
6250
|
}
|
|
7510
6251
|
}
|
|
@@ -7567,6 +6308,7 @@ const DocumentMediaReferenceInsert = createInsertSchema(documentMediaReferences,
|
|
|
7567
6308
|
//#region ../sql-pg/src/internal/documents/records.ts
|
|
7568
6309
|
const DocumentSnapshotRow = Schema.Struct({
|
|
7569
6310
|
authored: Schema.NullOr(DocumentRevisionSelect),
|
|
6311
|
+
created: Schema.optionalKey(Schema.NullOr(DocumentRevisionSelect)),
|
|
7570
6312
|
document: DocumentSelect,
|
|
7571
6313
|
head: Schema.NullOr(DocumentHeadSelect),
|
|
7572
6314
|
published: Schema.NullOr(DocumentRevisionSelect)
|
|
@@ -7583,11 +6325,18 @@ const decodeDocumentSnapshot = Effect.fn("DocumentStore.Drizzle.decodeSnapshot")
|
|
|
7583
6325
|
if (documentIds.has(row.document.id)) return yield* documentPersistenceError("DocumentStore.Drizzle.decodeSnapshot", /* @__PURE__ */ new Error(`Documents persistence returned duplicate document "${row.document.id}".`));
|
|
7584
6326
|
documentIds.add(row.document.id);
|
|
7585
6327
|
yield* validateSnapshotRow(row.document, row.head, row.authored, row.published);
|
|
7586
|
-
const authored = sourceRecord(row.authored, row.head.publishedRevisionId ?? void 0);
|
|
6328
|
+
const authored = Object.assign({}, sourceRecord(row.authored, row.head.publishedRevisionId ?? void 0), row.created ? { createdBy: revisionActor(row.created) } : void 0);
|
|
7587
6329
|
if (row.document.deletedAt === null) {
|
|
7588
6330
|
active.push(authored);
|
|
7589
6331
|
if (row.published) published.push(sourceRecord(row.published, row.published.id));
|
|
7590
|
-
} else
|
|
6332
|
+
} else {
|
|
6333
|
+
const details = yield* Schema.decodeEffect(Schema.Struct({
|
|
6334
|
+
title: DeletedDocument.fields.title,
|
|
6335
|
+
path: DeletedDocument.fields.path,
|
|
6336
|
+
trashBatchId: DeletedDocument.fields.trashBatchId
|
|
6337
|
+
}))(row.authored.activityDetails).pipe(Effect.mapError((cause) => documentPersistenceError("DocumentStore.decodeTrash", cause)));
|
|
6338
|
+
deleted.push(Object.assign({}, authored, details, { deletedAt: row.document.deletedAt }));
|
|
6339
|
+
}
|
|
7591
6340
|
}
|
|
7592
6341
|
active.sort(compareSources);
|
|
7593
6342
|
deleted.sort(compareSources);
|
|
@@ -7643,6 +6392,8 @@ function validateSnapshotRow(document, head, authored, published) {
|
|
|
7643
6392
|
}
|
|
7644
6393
|
function sourceRecord(revision, publishedRevisionId) {
|
|
7645
6394
|
const record = {
|
|
6395
|
+
editedAt: revision.createdAt,
|
|
6396
|
+
editedBy: revisionActor(revision),
|
|
7646
6397
|
documentId: revision.documentId,
|
|
7647
6398
|
markdown: revision.markdown,
|
|
7648
6399
|
revisionId: revision.id,
|
|
@@ -7661,6 +6412,14 @@ function documentPersistenceError(operation, cause) {
|
|
|
7661
6412
|
operation
|
|
7662
6413
|
});
|
|
7663
6414
|
}
|
|
6415
|
+
function revisionActor(revision) {
|
|
6416
|
+
return {
|
|
6417
|
+
id: revision.actorId,
|
|
6418
|
+
name: revision.actorName,
|
|
6419
|
+
email: revision.actorEmail,
|
|
6420
|
+
roles: revision.actorRoles
|
|
6421
|
+
};
|
|
6422
|
+
}
|
|
7664
6423
|
|
|
7665
6424
|
//#endregion
|
|
7666
6425
|
//#region ../sql-pg/src/internal/database/media-reference-lock.ts
|
|
@@ -7766,6 +6525,7 @@ function validMediaObjectKey(column) {
|
|
|
7766
6525
|
//#endregion
|
|
7767
6526
|
//#region ../sql-pg/src/internal/documents/document-store.ts
|
|
7768
6527
|
const authoredRevision = alias(documentRevisions, "authored_revision");
|
|
6528
|
+
const createdRevision = alias(documentRevisions, "created_revision");
|
|
7769
6529
|
const publishedRevision = alias(documentRevisions, "published_revision");
|
|
7770
6530
|
const RepositoryDomainError = Schema.Union([
|
|
7771
6531
|
DocumentGenerationConflict,
|
|
@@ -7779,7 +6539,7 @@ const isRepositoryDomainError = Schema.is(RepositoryDomainError);
|
|
|
7779
6539
|
const isDocumentNotFound = Schema.is(DocumentNotFound);
|
|
7780
6540
|
const isDocumentMediaUnavailable = Schema.is(DocumentMediaUnavailable);
|
|
7781
6541
|
const isDocumentPersistenceError = Schema.is(DocumentPersistenceError);
|
|
7782
|
-
const systemActivity = ActivityContext.make({
|
|
6542
|
+
const systemActivity$1 = ActivityContext.make({
|
|
7783
6543
|
actor: {
|
|
7784
6544
|
email: "system@cms.invalid",
|
|
7785
6545
|
id: "system",
|
|
@@ -7800,19 +6560,26 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
7800
6560
|
});
|
|
7801
6561
|
}
|
|
7802
6562
|
const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
|
|
7803
|
-
const
|
|
7804
|
-
const
|
|
7805
|
-
yield*
|
|
7806
|
-
yield*
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
6563
|
+
const plans = [input.plan, ...input.additionalPlans ?? []];
|
|
6564
|
+
const references = plans.flatMap((plan) => input.mediaReferences.filter((reference) => reference.documentId === plan.documentId));
|
|
6565
|
+
if (references.length !== input.mediaReferences.length) return yield* persistenceFailure$3("DocumentStore.commit", /* @__PURE__ */ new Error("Unexpected media reference owner."));
|
|
6566
|
+
yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
|
|
6567
|
+
yield* rejectUnavailableMedia(transaction, references);
|
|
6568
|
+
const activity = input.activity ?? systemActivity$1;
|
|
6569
|
+
for (const plan of plans) {
|
|
6570
|
+
yield* applyDocumentCommitPlan(transaction, plan, activity);
|
|
6571
|
+
yield* replaceDocumentMediaReferences(transaction, plan.documentId, references.filter((reference) => reference.documentId === plan.documentId));
|
|
6572
|
+
}
|
|
6573
|
+
const snapshot = yield* readSnapshot$1(transaction, state.generation);
|
|
6574
|
+
for (const plan of plans) {
|
|
6575
|
+
const affected = yield* sourceForMutation(snapshot, plan.documentId, plan.action === "document.remove");
|
|
6576
|
+
yield* recordDocumentActivity(transaction, affected, plan, activity);
|
|
6577
|
+
}
|
|
7811
6578
|
const source = yield* sourceForMutation(snapshot, input.plan.documentId, input.plan.action === "document.remove");
|
|
7812
|
-
yield* recordDocumentActivity(transaction, source, input.plan, activity);
|
|
7813
6579
|
return {
|
|
7814
6580
|
mutation: DocumentMutationResult.make({
|
|
7815
6581
|
action: input.plan.action,
|
|
6582
|
+
affectedDocumentIds: plans.map((plan) => plan.documentId),
|
|
7816
6583
|
documentId: source.documentId,
|
|
7817
6584
|
generation: snapshot.generation,
|
|
7818
6585
|
revisionId: source.revisionId,
|
|
@@ -7825,15 +6592,15 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
7825
6592
|
const load = Effect.fn("DocumentStore.Drizzle.load")((after) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7826
6593
|
const generation = yield* readGeneration(transaction);
|
|
7827
6594
|
if (after === generation.toString()) return void 0;
|
|
7828
|
-
return yield* readSnapshot(transaction, generation);
|
|
6595
|
+
return yield* readSnapshot$1(transaction, generation);
|
|
7829
6596
|
}), {
|
|
7830
6597
|
accessMode: "read only",
|
|
7831
6598
|
isolationLevel: "repeatable read"
|
|
7832
|
-
}).pipe(mapPersistenceFailure$
|
|
6599
|
+
}).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.load")));
|
|
7833
6600
|
const synchronizeMediaReferences = Effect.fn("DocumentStore.Drizzle.synchronizeMediaReferences")((input) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7834
6601
|
const stateRows = yield* transaction.select().from(cmsState).where(eq(cmsState.singleton, true)).limit(2).for("update");
|
|
7835
6602
|
const stateRow = stateRows[0];
|
|
7836
|
-
if (stateRows.length !== 1 || !stateRow) return yield* persistenceFailure$
|
|
6603
|
+
if (stateRows.length !== 1 || !stateRow) return yield* persistenceFailure$3("DocumentStore.Drizzle.synchronizeMediaReferences", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${stateRows.length}.`));
|
|
7837
6604
|
const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
|
|
7838
6605
|
if (state.generation.toString() !== input.expectedGeneration) return yield* new DocumentGenerationConflict({
|
|
7839
6606
|
actualGeneration: DocumentGeneration.make(String(state.generation)),
|
|
@@ -7841,7 +6608,7 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
7841
6608
|
});
|
|
7842
6609
|
const references = uniqueMediaReferences(input.references);
|
|
7843
6610
|
yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
|
|
7844
|
-
yield*
|
|
6611
|
+
yield* rejectUnavailableMedia(transaction, references);
|
|
7845
6612
|
yield* transaction.delete(documentMediaReferences);
|
|
7846
6613
|
if (references.length > 0) yield* transaction.insert(documentMediaReferences).values(references.map(referenceValues));
|
|
7847
6614
|
})).pipe(mapSynchronizationFailure("DocumentStore.Drizzle.synchronizeMediaReferences")));
|
|
@@ -7857,12 +6624,12 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
|
|
|
7857
6624
|
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;
|
|
7858
6625
|
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);
|
|
7859
6626
|
const total = (yield* transaction.select({ value: count() }).from(documentRevisions).where(eq(documentRevisions.documentId, query.documentId)))[0]?.value;
|
|
7860
|
-
if (total === void 0) return yield* persistenceFailure$
|
|
6627
|
+
if (total === void 0) return yield* persistenceFailure$3("DocumentStore.Drizzle.revisions", /* @__PURE__ */ new Error("Documents persistence did not return a history total."));
|
|
7861
6628
|
return revisionPage(yield* Effect.forEach(rows, decodeDocumentRevision), query.limit, total);
|
|
7862
6629
|
}), {
|
|
7863
6630
|
accessMode: "read only",
|
|
7864
6631
|
isolationLevel: "repeatable read"
|
|
7865
|
-
}).pipe(mapPersistenceFailure$
|
|
6632
|
+
}).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.revisions")));
|
|
7866
6633
|
return DocumentStore.of({
|
|
7867
6634
|
commit,
|
|
7868
6635
|
load,
|
|
@@ -7901,27 +6668,28 @@ const recordDocumentActivity = Effect.fn("DocumentStore.Drizzle.recordActivity")
|
|
|
7901
6668
|
targetId: source.documentId,
|
|
7902
6669
|
targetKind: "document"
|
|
7903
6670
|
};
|
|
7904
|
-
if (plan.action !== "document.publish") values.id = source.revisionId;
|
|
6671
|
+
if (plan.action !== "document.publish" && plan.action !== "document.unpublish") values.id = source.revisionId;
|
|
7905
6672
|
yield* database.insert(activityEntries).values(values);
|
|
7906
6673
|
});
|
|
7907
|
-
const readSnapshot = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
|
|
6674
|
+
const readSnapshot$1 = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
|
|
7908
6675
|
const rows = yield* database.select({
|
|
7909
6676
|
authored: authoredRevision,
|
|
6677
|
+
created: createdRevision,
|
|
7910
6678
|
document: documentsTable,
|
|
7911
6679
|
head: documentHeads,
|
|
7912
6680
|
published: publishedRevision
|
|
7913
|
-
}).from(documentsTable).leftJoin(documentHeads, eq(documentHeads.documentId, documentsTable.id)).leftJoin(authoredRevision, and(eq(authoredRevision.documentId, documentHeads.documentId), eq(authoredRevision.id, documentHeads.authoredRevisionId))).leftJoin(publishedRevision, and(eq(publishedRevision.documentId, documentHeads.documentId), eq(publishedRevision.id, documentHeads.publishedRevisionId)));
|
|
6681
|
+
}).from(documentsTable).leftJoin(createdRevision, and(eq(createdRevision.documentId, documentsTable.id), eq(createdRevision.action, "document.create"))).leftJoin(documentHeads, eq(documentHeads.documentId, documentsTable.id)).leftJoin(authoredRevision, and(eq(authoredRevision.documentId, documentHeads.documentId), eq(authoredRevision.id, documentHeads.authoredRevisionId))).leftJoin(publishedRevision, and(eq(publishedRevision.documentId, documentHeads.documentId), eq(publishedRevision.id, documentHeads.publishedRevisionId)));
|
|
7914
6682
|
return yield* decodeDocumentSnapshot(rows, generation);
|
|
7915
6683
|
});
|
|
7916
6684
|
const readGeneration = Effect.fn("DocumentStore.Drizzle.readGeneration")(function* (database) {
|
|
7917
6685
|
const rows = yield* database.select().from(cmsState);
|
|
7918
6686
|
const row = rows[0];
|
|
7919
|
-
if (rows.length !== 1 || !row) return yield* persistenceFailure$
|
|
6687
|
+
if (rows.length !== 1 || !row) return yield* persistenceFailure$3("DocumentStore.Drizzle.readGeneration", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${rows.length}.`));
|
|
7920
6688
|
return (yield* Schema.decodeUnknownEffect(CmsStateSelect)(row)).generation;
|
|
7921
6689
|
});
|
|
7922
6690
|
const sourceForMutation = Effect.fn("DocumentStore.Drizzle.sourceForMutation")(function* (snapshot, documentId, deleted) {
|
|
7923
6691
|
const source = (deleted ? snapshot.deleted : snapshot.active).find((candidate) => candidate.documentId === documentId);
|
|
7924
|
-
if (!source) return yield* persistenceFailure$
|
|
6692
|
+
if (!source) return yield* persistenceFailure$3("DocumentStore.Drizzle.sourceForMutation", /* @__PURE__ */ new Error(`Committed document "${documentId}" is missing from its resulting snapshot.`));
|
|
7925
6693
|
return source;
|
|
7926
6694
|
});
|
|
7927
6695
|
function mapCommitFailure(operation) {
|
|
@@ -7933,18 +6701,27 @@ function mapSynchronizationFailure(operation) {
|
|
|
7933
6701
|
return documentPersistenceError(operation, cause);
|
|
7934
6702
|
}));
|
|
7935
6703
|
}
|
|
7936
|
-
const
|
|
7937
|
-
if (references.some((reference) => reference.documentId !== documentId)) return yield* persistenceFailure$2("DocumentStore.Drizzle.referencesForDocument", /* @__PURE__ */ new Error(`The media-reference projection does not belong exclusively to document "${documentId}".`));
|
|
7938
|
-
return uniqueMediaReferences(references);
|
|
7939
|
-
});
|
|
7940
|
-
const rejectDeletingMedia = Effect.fn("DocumentStore.Drizzle.rejectDeletingMedia")(function* (transaction, references) {
|
|
6704
|
+
const rejectUnavailableMedia = Effect.fn("DocumentStore.Drizzle.rejectUnavailableMedia")(function* (transaction, references) {
|
|
7941
6705
|
const keys = [...new Set(references.map((reference) => reference.objectKey))];
|
|
6706
|
+
const mediaIds = [...new Set(references.flatMap((reference) => reference.mediaId ? [reference.mediaId] : []))];
|
|
7942
6707
|
if (keys.length === 0) return;
|
|
7943
|
-
|
|
7944
|
-
|
|
6708
|
+
if (mediaIds.length > 0) {
|
|
6709
|
+
const identityRows = yield* transaction.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
|
|
6710
|
+
yield* lockMediaReferenceKeys(transaction, yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(identityRows.map((row) => row.objectKey)));
|
|
6711
|
+
}
|
|
6712
|
+
const rows = yield* transaction.select({
|
|
6713
|
+
id: mediaAssets.id,
|
|
6714
|
+
objectKey: mediaAssets.objectKey,
|
|
6715
|
+
status: mediaAssets.status
|
|
6716
|
+
}).from(mediaAssets).where(mediaIds.length > 0 ? or(inArray(mediaAssets.objectKey, keys), inArray(mediaAssets.id, mediaIds)) : inArray(mediaAssets.objectKey, keys));
|
|
6717
|
+
const deletingKeys = new Set(yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(rows.filter((row) => row.status === "deleting").map((row) => row.objectKey)));
|
|
6718
|
+
const unavailable = references.filter((reference) => {
|
|
6719
|
+
if (!reference.mediaId) return deletingKeys.has(reference.objectKey);
|
|
6720
|
+
const asset = rows.find((row) => row.id === reference.mediaId);
|
|
6721
|
+
return !asset || asset.status !== "ready" || asset.objectKey !== reference.objectKey;
|
|
6722
|
+
});
|
|
7945
6723
|
if (unavailable.length === 0) return;
|
|
7946
|
-
const
|
|
7947
|
-
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));
|
|
6724
|
+
const affected = unavailable.sort((left, right) => left.documentId === right.documentId ? left.objectKey.localeCompare(right.objectKey) : left.documentId.localeCompare(right.documentId));
|
|
7948
6725
|
const first = affected[0];
|
|
7949
6726
|
if (!first) return;
|
|
7950
6727
|
return yield* new DocumentMediaUnavailable({
|
|
@@ -7968,7 +6745,7 @@ function referenceValues(reference) {
|
|
|
7968
6745
|
sourcePath: reference.sourcePath
|
|
7969
6746
|
};
|
|
7970
6747
|
}
|
|
7971
|
-
function mapPersistenceFailure$
|
|
6748
|
+
function mapPersistenceFailure$3(operation) {
|
|
7972
6749
|
return (effect) => effect.pipe(Effect.mapError((cause) => isDocumentPersistenceError(cause) ? cause : documentPersistenceError(operation, cause)));
|
|
7973
6750
|
}
|
|
7974
6751
|
function mapRevisionFailure(operation) {
|
|
@@ -7977,7 +6754,7 @@ function mapRevisionFailure(operation) {
|
|
|
7977
6754
|
return documentPersistenceError(operation, cause);
|
|
7978
6755
|
}));
|
|
7979
6756
|
}
|
|
7980
|
-
function persistenceFailure$
|
|
6757
|
+
function persistenceFailure$3(operation, cause) {
|
|
7981
6758
|
return Effect.fail(documentPersistenceError(operation, cause));
|
|
7982
6759
|
}
|
|
7983
6760
|
|
|
@@ -8040,7 +6817,7 @@ const initializationStoreLayer = Layer.effect(InitializationStore, Effect.gen(fu
|
|
|
8040
6817
|
const locks = yield* PostgresAdvisoryLock;
|
|
8041
6818
|
const claim = Effect.fn("InitializationStore.Postgres.claim")((value) => database.update(initializationState).set({
|
|
8042
6819
|
changeId: value.changeId,
|
|
8043
|
-
claim: Schema.
|
|
6820
|
+
claim: Result.getOrThrow(Schema.encodeResult(InitializationClaim)(value)),
|
|
8044
6821
|
state: "claiming"
|
|
8045
6822
|
}).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")));
|
|
8046
6823
|
const complete = Effect.fn("InitializationStore.Postgres.complete")((changeId) => transition(database, changeId, "ready"));
|
|
@@ -8092,6 +6869,53 @@ function mapUnavailable(operation) {
|
|
|
8092
6869
|
})));
|
|
8093
6870
|
}
|
|
8094
6871
|
|
|
6872
|
+
//#endregion
|
|
6873
|
+
//#region ../sql-pg/src/internal/seo/tables.ts
|
|
6874
|
+
const seoRevisions = pgTable("seo_revisions", {
|
|
6875
|
+
id: uuid().defaultRandom().primaryKey(),
|
|
6876
|
+
settings: jsonb().$type().notNull(),
|
|
6877
|
+
actorId: text("actor_id").notNull(),
|
|
6878
|
+
actorEmail: text("actor_email").notNull(),
|
|
6879
|
+
actorName: text("actor_name").notNull(),
|
|
6880
|
+
actorRoles: jsonb("actor_roles").$type().notNull(),
|
|
6881
|
+
requestId: uuid("request_id").notNull(),
|
|
6882
|
+
reason: text(),
|
|
6883
|
+
createdAt: timestamp("created_at", {
|
|
6884
|
+
mode: "string",
|
|
6885
|
+
withTimezone: true
|
|
6886
|
+
}).default(transactionTimestamp).notNull()
|
|
6887
|
+
}, (table) => [
|
|
6888
|
+
check("seo_revisions_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
|
|
6889
|
+
check("seo_revisions_settings_check", sql`jsonb_typeof(${table.settings}) = 'object'`),
|
|
6890
|
+
index("seo_revisions_history").on(table.createdAt.desc(), table.id.desc())
|
|
6891
|
+
]);
|
|
6892
|
+
const seoState = pgTable("seo_state", {
|
|
6893
|
+
singleton: boolean().default(true).primaryKey(),
|
|
6894
|
+
workingRevisionId: uuid("working_revision_id").notNull(),
|
|
6895
|
+
publishedRevisionId: uuid("published_revision_id").notNull()
|
|
6896
|
+
}, (table) => [
|
|
6897
|
+
check("seo_state_singleton_check", sql`${table.singleton}`),
|
|
6898
|
+
foreignKey({
|
|
6899
|
+
columns: [table.workingRevisionId],
|
|
6900
|
+
foreignColumns: [seoRevisions.id],
|
|
6901
|
+
name: "seo_state_working_revision_fkey"
|
|
6902
|
+
}).onDelete("restrict"),
|
|
6903
|
+
foreignKey({
|
|
6904
|
+
columns: [table.publishedRevisionId],
|
|
6905
|
+
foreignColumns: [seoRevisions.id],
|
|
6906
|
+
name: "seo_state_published_revision_fkey"
|
|
6907
|
+
}).onDelete("restrict")
|
|
6908
|
+
]);
|
|
6909
|
+
const seoMediaReferences = pgTable("seo_media_references", {
|
|
6910
|
+
id: uuid().defaultRandom().primaryKey(),
|
|
6911
|
+
scope: text({ enum: ["working", "published"] }).notNull(),
|
|
6912
|
+
mediaId: uuid("media_id").notNull().references(() => mediaAssets.id, { onDelete: "restrict" })
|
|
6913
|
+
}, (table) => [
|
|
6914
|
+
check("seo_media_references_scope_check", sql`${table.scope} in ('working', 'published')`),
|
|
6915
|
+
unique("seo_media_references_scope_media_unique").on(table.scope, table.mediaId),
|
|
6916
|
+
index("seo_media_references_media").on(table.mediaId)
|
|
6917
|
+
]);
|
|
6918
|
+
|
|
8095
6919
|
//#endregion
|
|
8096
6920
|
//#region ../sql-pg/src/internal/media/row-schema.ts
|
|
8097
6921
|
const mediaRefinements = {
|
|
@@ -8159,14 +6983,14 @@ const isMediaReferenced = Schema.is(MediaReferenced);
|
|
|
8159
6983
|
const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
8160
6984
|
const database = yield* PgDrizzle.makeWithDefaults();
|
|
8161
6985
|
const reserve = Effect.fn("MediaStore.Drizzle.reserve")((record, operation) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8162
|
-
if (record.status !== "uploading") return yield* persistenceFailure$
|
|
6986
|
+
if (record.status !== "uploading") return yield* persistenceFailure$2("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`A media reservation must start as uploading, received "${record.status}".`));
|
|
8163
6987
|
yield* beginChange(transaction, mediaUploadChange(record, operation)).pipe(Effect.flatMap(requireUnresolvedChange));
|
|
8164
6988
|
const rows = yield* transaction.insert(mediaAssets).values(recordValues(record)).onConflictDoNothing().returning();
|
|
8165
6989
|
if (rows[0]) return yield* decodeMediaRecord(rows[0]);
|
|
8166
6990
|
const existing = yield* rowById(transaction, record.id);
|
|
8167
6991
|
if (existing && existing.status !== "deleting" && isSameReservation(existing, record)) return existing;
|
|
8168
|
-
return yield* persistenceFailure$
|
|
8169
|
-
})).pipe(mapPersistenceFailure$
|
|
6992
|
+
return yield* persistenceFailure$2("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`Media reservation "${record.id}" conflicts with durable media state.`));
|
|
6993
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.reserve")));
|
|
8170
6994
|
const activate = Effect.fn("MediaStore.Drizzle.activate")((id) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8171
6995
|
const existing = yield* lockedRowById(transaction, id);
|
|
8172
6996
|
if (existing?.status === "ready") return existing;
|
|
@@ -8184,11 +7008,11 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8184
7008
|
kind: "applied"
|
|
8185
7009
|
});
|
|
8186
7010
|
return activated;
|
|
8187
|
-
})).pipe(mapPersistenceFailure$
|
|
7011
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.activate")));
|
|
8188
7012
|
const find = Effect.fn("MediaStore.Drizzle.find")((id) => Effect.gen(function* () {
|
|
8189
7013
|
const rows = yield* database.select().from(mediaAssets).where(and(eq(mediaAssets.id, id), eq(mediaAssets.status, "ready"))).limit(1);
|
|
8190
7014
|
return rows[0] ? Option.some(yield* decodeMediaRecord(rows[0])) : Option.none();
|
|
8191
|
-
}).pipe(mapPersistenceFailure$
|
|
7015
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.find")));
|
|
8192
7016
|
const beginDelete = Effect.fn("MediaStore.Drizzle.beginDelete")((id, operation) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8193
7017
|
const existing = yield* lockedRowById(transaction, id);
|
|
8194
7018
|
if (!existing) return yield* new MediaNotFound({ mediaId: id });
|
|
@@ -8201,7 +7025,13 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8201
7025
|
yield* lockMediaReferenceKeys(transaction, [existing.objectKey]);
|
|
8202
7026
|
const referenceRows = yield* transaction.select().from(documentMediaReferences).where(eq(documentMediaReferences.objectKey, existing.objectKey)).orderBy(asc(documentMediaReferences.documentId), asc(documentMediaReferences.sourcePath), asc(documentMediaReferences.nodePath));
|
|
8203
7027
|
const references = yield* decodeMediaReferences(referenceRows);
|
|
8204
|
-
|
|
7028
|
+
const seoReferenceRows = yield* transaction.select({ scope: seoMediaReferences.scope }).from(seoMediaReferences).where(eq(seoMediaReferences.mediaId, id)).orderBy(asc(seoMediaReferences.scope));
|
|
7029
|
+
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)));
|
|
7030
|
+
if (references.length > 0 || seoScopes.length > 0) return yield* new MediaReferenced(seoScopes.length > 0 ? {
|
|
7031
|
+
mediaId: id,
|
|
7032
|
+
references,
|
|
7033
|
+
seoScopes
|
|
7034
|
+
} : {
|
|
8205
7035
|
mediaId: id,
|
|
8206
7036
|
references
|
|
8207
7037
|
});
|
|
@@ -8222,7 +7052,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8222
7052
|
const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
8223
7053
|
if (existing && isSameReplacement(existing, replacement)) return existing;
|
|
8224
7054
|
return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
8225
|
-
})).pipe(mapStoreFailure$
|
|
7055
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.beginReplace")));
|
|
8226
7056
|
const markReplaceStaged = Effect.fn("MediaStore.Drizzle.markReplaceStaged")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8227
7057
|
yield* lockedRowById(transaction, replacement.mediaId);
|
|
8228
7058
|
const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
@@ -8230,14 +7060,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8230
7060
|
if (existing.staged) return existing;
|
|
8231
7061
|
const rows = yield* transaction.update(mediaReplacements).set({ staged: true }).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning();
|
|
8232
7062
|
if (rows[0]) return yield* decodeMediaReplacement(rows[0]);
|
|
8233
|
-
return yield* persistenceFailure$
|
|
8234
|
-
})).pipe(mapStoreFailure$
|
|
7063
|
+
return yield* persistenceFailure$2("MediaStore.Drizzle.markReplaceStaged", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
|
|
7064
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.markReplaceStaged")));
|
|
8235
7065
|
const finishReplace = Effect.fn("MediaStore.Drizzle.finishReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8236
7066
|
const asset = yield* lockedRowById(transaction, replacement.mediaId);
|
|
8237
7067
|
const pendingReplacement = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
8238
7068
|
if (!pendingReplacement) {
|
|
8239
7069
|
if (asset && isCompletedReplacement(asset, replacement)) return asset;
|
|
8240
|
-
return yield* persistenceFailure$
|
|
7070
|
+
return yield* persistenceFailure$2("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" cannot be replayed from durable state.`));
|
|
8241
7071
|
}
|
|
8242
7072
|
if (!isSameReplacement(pendingReplacement, replacement)) return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
8243
7073
|
if (!asset || asset.status !== "ready" || asset.objectKey !== replacement.objectKey || asset.mimeType !== replacement.mimeType) return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
@@ -8250,7 +7080,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8250
7080
|
updatedAt: transactionTimestamp
|
|
8251
7081
|
}).where(and(eq(mediaAssets.id, replacement.mediaId), eq(mediaAssets.status, "ready"))).returning())[0];
|
|
8252
7082
|
if (!replaced) return yield* transitionFailure("MediaStore.Drizzle.finishReplace", replacement.mediaId, asset.status);
|
|
8253
|
-
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$
|
|
7083
|
+
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.`));
|
|
8254
7084
|
const decoded = yield* decodeMediaRecord(replaced);
|
|
8255
7085
|
const intent = yield* decodeMediaChangeIntent(change);
|
|
8256
7086
|
yield* resolveChange(transaction, change, {
|
|
@@ -8258,7 +7088,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8258
7088
|
kind: "applied"
|
|
8259
7089
|
});
|
|
8260
7090
|
return decoded;
|
|
8261
|
-
})).pipe(mapStoreFailure$
|
|
7091
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.finishReplace")));
|
|
8262
7092
|
const cancelReplace = Effect.fn("MediaStore.Drizzle.cancelReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8263
7093
|
yield* lockedRowById(transaction, replacement.mediaId);
|
|
8264
7094
|
const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
@@ -8267,7 +7097,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8267
7097
|
const change = yield* requirePendingChange(transaction, mediaSubject(replacement.mediaId), "media.replaced");
|
|
8268
7098
|
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 });
|
|
8269
7099
|
yield* resolveChange(transaction, change, { kind: "cancelled" });
|
|
8270
|
-
})).pipe(mapStoreFailure$
|
|
7100
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.cancelReplace")));
|
|
8271
7101
|
const finishDelete = Effect.fn("MediaStore.Drizzle.finishDelete")((id) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8272
7102
|
const existing = yield* lockedRowById(transaction, id);
|
|
8273
7103
|
if (!existing) return;
|
|
@@ -8279,7 +7109,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8279
7109
|
details: mediaChangeDetails(intent),
|
|
8280
7110
|
kind: "applied"
|
|
8281
7111
|
});
|
|
8282
|
-
})).pipe(mapPersistenceFailure$
|
|
7112
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.finishDelete")));
|
|
8283
7113
|
const abandonUpload = Effect.fn("MediaStore.Drizzle.abandonUpload")((id) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8284
7114
|
const existing = yield* lockedRowById(transaction, id);
|
|
8285
7115
|
if (!existing) return;
|
|
@@ -8287,15 +7117,15 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8287
7117
|
const change = yield* requirePendingChange(transaction, mediaSubject(id), "media.uploaded");
|
|
8288
7118
|
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);
|
|
8289
7119
|
yield* resolveChange(transaction, change, { kind: "cancelled" });
|
|
8290
|
-
})).pipe(mapPersistenceFailure$
|
|
7120
|
+
})).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.abandonUpload")));
|
|
8291
7121
|
const pending = Effect.fn("MediaStore.Drizzle.pending")((query) => Effect.gen(function* () {
|
|
8292
|
-
const rows = yield* database.select().from(mediaAssets).where(and(ne(mediaAssets.status, "ready"), lte(mediaAssets.updatedAt, DateTime.formatIso(query.before))
|
|
7122
|
+
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);
|
|
8293
7123
|
return yield* Effect.forEach(rows, decodeMediaRecord);
|
|
8294
|
-
}).pipe(mapPersistenceFailure$
|
|
7124
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pending")));
|
|
8295
7125
|
const pendingReplacements = Effect.fn("MediaStore.Drizzle.pendingReplacements")((query) => Effect.gen(function* () {
|
|
8296
|
-
const rows = yield* database.select().from(mediaReplacements).where(lte(mediaReplacements.updatedAt, DateTime.formatIso(query.before))
|
|
7126
|
+
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);
|
|
8297
7127
|
return yield* Effect.forEach(rows, decodeMediaReplacement);
|
|
8298
|
-
}).pipe(mapPersistenceFailure$
|
|
7128
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pendingReplacements")));
|
|
8299
7129
|
const inspectReplacement = Effect.fn("MediaStore.Drizzle.inspectReplacement")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8300
7130
|
const asset = yield* lockedRowById(transaction, replacement.mediaId);
|
|
8301
7131
|
const pending = yield* replacementByMediaId(transaction, replacement.mediaId);
|
|
@@ -8304,17 +7134,17 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8304
7134
|
return yield* new MediaConflict({ mediaId: replacement.mediaId });
|
|
8305
7135
|
}
|
|
8306
7136
|
return asset && isCompletedReplacement(asset, replacement) ? "completed" : "absent";
|
|
8307
|
-
})).pipe(mapStoreFailure$
|
|
7137
|
+
})).pipe(mapStoreFailure$2("MediaStore.Drizzle.inspectReplacement")));
|
|
8308
7138
|
const inventory = Effect.gen(function* () {
|
|
8309
7139
|
const rows = yield* database.select().from(mediaAssets).where(eq(mediaAssets.status, "ready")).orderBy(asc(mediaAssets.objectKey));
|
|
8310
7140
|
return yield* Effect.forEach(rows, decodeMediaRecord);
|
|
8311
|
-
}).pipe(mapPersistenceFailure$
|
|
7141
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.inventory"), Effect.withSpan("MediaStore.Drizzle.inventory"));
|
|
8312
7142
|
const knownObjectKeys = Effect.gen(function* () {
|
|
8313
7143
|
const assetRows = yield* database.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).orderBy(asc(mediaAssets.objectKey));
|
|
8314
7144
|
const replacementRows = yield* database.select({ objectKey: mediaReplacements.stagingObjectKey }).from(mediaReplacements).orderBy(asc(mediaReplacements.stagingObjectKey));
|
|
8315
7145
|
const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))([...assetRows, ...replacementRows].map((row) => row.objectKey));
|
|
8316
7146
|
return new Set(objectKeys);
|
|
8317
|
-
}).pipe(mapPersistenceFailure$
|
|
7147
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.knownObjectKeys"), Effect.withSpan("MediaStore.Drizzle.knownObjectKeys"));
|
|
8318
7148
|
const search = Effect.fn("MediaStore.Drizzle.search")((query) => database.transaction((transaction) => Effect.gen(function* () {
|
|
8319
7149
|
const normalizedSearch = query.search?.trim();
|
|
8320
7150
|
const pattern = normalizedSearch ? `%${escapeLike(normalizedSearch)}%` : void 0;
|
|
@@ -8323,14 +7153,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
|
|
|
8323
7153
|
const rows = yield* transaction.select().from(mediaAssets).where(and(filters, cursorFilter)).orderBy(desc(mediaAssets.createdAt), desc(mediaAssets.id)).limit(query.limit + 1);
|
|
8324
7154
|
const folderRows = yield* transaction.selectDistinct({ folder: mediaAssets.folder }).from(mediaAssets).where(and(eq(mediaAssets.status, "ready"), ne(mediaAssets.folder, ""))).orderBy(asc(mediaAssets.folder));
|
|
8325
7155
|
const total = (yield* transaction.select({ value: count() }).from(mediaAssets).where(filters))[0]?.value;
|
|
8326
|
-
if (total === void 0) return yield* persistenceFailure$
|
|
7156
|
+
if (total === void 0) return yield* persistenceFailure$2("MediaStore.Drizzle.search", /* @__PURE__ */ new Error("Media persistence did not return a search total."));
|
|
8327
7157
|
const decoded = yield* Effect.forEach(rows, decodeMediaRecord);
|
|
8328
7158
|
const folders = yield* Schema.decodeEffect(Schema.Array(MediaFolder))(folderRows.map((row) => row.folder)).pipe(Effect.mapError((cause) => mediaPersistenceError("MediaStore.Drizzle.search", cause)));
|
|
8329
7159
|
return mediaPage(decoded, query.limit, folders, total);
|
|
8330
7160
|
}), {
|
|
8331
7161
|
accessMode: "read only",
|
|
8332
7162
|
isolationLevel: "repeatable read"
|
|
8333
|
-
}).pipe(mapPersistenceFailure$
|
|
7163
|
+
}).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.search")));
|
|
8334
7164
|
return MediaStore.of({
|
|
8335
7165
|
abandonUpload,
|
|
8336
7166
|
activate,
|
|
@@ -8415,45 +7245,230 @@ const replacementByMediaId = Effect.fn("MediaStore.Drizzle.replacementByMediaId"
|
|
|
8415
7245
|
return rows[0] ? yield* decodeMediaReplacement(rows[0]) : void 0;
|
|
8416
7246
|
});
|
|
8417
7247
|
function transitionFailure(operation, id, actualStatus) {
|
|
8418
|
-
return persistenceFailure$
|
|
7248
|
+
return persistenceFailure$2(operation, /* @__PURE__ */ new Error(actualStatus === void 0 ? `Media record "${id}" does not exist.` : `Media record "${id}" cannot transition from "${actualStatus}".`));
|
|
8419
7249
|
}
|
|
8420
|
-
function mapPersistenceFailure$
|
|
7250
|
+
function mapPersistenceFailure$2(operation) {
|
|
8421
7251
|
return (effect) => effect.pipe(Effect.mapError((cause) => isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
|
|
8422
7252
|
}
|
|
8423
|
-
function mapStoreFailure$
|
|
7253
|
+
function mapStoreFailure$2(operation) {
|
|
8424
7254
|
return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
|
|
8425
7255
|
}
|
|
8426
7256
|
function mapBeginDeleteFailure(operation) {
|
|
8427
7257
|
return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaNotFound(cause) || isMediaPersistenceError(cause) || isMediaReferenced(cause) ? cause : mediaPersistenceError(operation, cause)));
|
|
8428
7258
|
}
|
|
8429
|
-
function persistenceFailure$
|
|
7259
|
+
function persistenceFailure$2(operation, cause) {
|
|
8430
7260
|
return Effect.fail(mediaPersistenceError(operation, cause));
|
|
8431
7261
|
}
|
|
8432
7262
|
|
|
8433
7263
|
//#endregion
|
|
8434
|
-
//#region ../sql-pg/src/internal/
|
|
8435
|
-
const
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
7264
|
+
//#region ../sql-pg/src/internal/seo/seo-store.ts
|
|
7265
|
+
const SeoStoreDomainError = Schema.Union([
|
|
7266
|
+
SeoMediaUnavailable,
|
|
7267
|
+
SeoPersistenceError,
|
|
7268
|
+
SeoVersionConflict
|
|
7269
|
+
]);
|
|
7270
|
+
const isSeoStoreDomainError = Schema.is(SeoStoreDomainError);
|
|
7271
|
+
const isSeoPersistenceError = Schema.is(SeoPersistenceError);
|
|
7272
|
+
const systemActivity = {
|
|
7273
|
+
actor: {
|
|
7274
|
+
email: "system@cms.invalid",
|
|
7275
|
+
id: "system",
|
|
7276
|
+
name: "CMS System",
|
|
7277
|
+
roles: ["system"]
|
|
7278
|
+
},
|
|
7279
|
+
requestId: RequestId.make("00000000-0000-4000-8000-000000000000")
|
|
7280
|
+
};
|
|
7281
|
+
const seoStoreLayer = Layer.effect(SeoStore, Effect.gen(function* () {
|
|
7282
|
+
const database = yield* PgDrizzle.makeWithDefaults();
|
|
7283
|
+
const initialize = Effect.fn("SeoStore.Drizzle.initialize")((defaults) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7284
|
+
yield* transaction.execute(sql`SELECT pg_advisory_xact_lock(hashtext('cms:seo:singleton:v1'))`);
|
|
7285
|
+
const current = yield* readSnapshot(transaction);
|
|
7286
|
+
if (current) return current;
|
|
7287
|
+
yield* ensureMediaAvailable(transaction, defaults).pipe(Effect.mapError((cause) => new SeoPersistenceError({
|
|
7288
|
+
cause,
|
|
7289
|
+
operation: "SeoStore.Drizzle.initializeDefaults"
|
|
7290
|
+
})));
|
|
7291
|
+
const revision = yield* insertRevision(transaction, defaults, systemActivity);
|
|
7292
|
+
yield* transaction.insert(seoState).values({
|
|
7293
|
+
publishedRevisionId: revision.revisionId,
|
|
7294
|
+
workingRevisionId: revision.revisionId
|
|
8452
7295
|
});
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
7296
|
+
yield* replaceMediaReferences(transaction, "working", defaults);
|
|
7297
|
+
yield* replaceMediaReferences(transaction, "published", defaults);
|
|
7298
|
+
return {
|
|
7299
|
+
published: revision,
|
|
7300
|
+
working: revision
|
|
7301
|
+
};
|
|
7302
|
+
})).pipe(mapStoreFailure$1("SeoStore.Drizzle.initialize")));
|
|
7303
|
+
const load = database.transaction((transaction) => readSnapshot(transaction), {
|
|
7304
|
+
accessMode: "read only",
|
|
7305
|
+
isolationLevel: "repeatable read"
|
|
7306
|
+
}).pipe(mapPersistenceFailure$1("SeoStore.Drizzle.load"));
|
|
7307
|
+
const save = Effect.fn("SeoStore.Drizzle.save")((input) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7308
|
+
const current = yield* lockedSnapshot(transaction);
|
|
7309
|
+
yield* requireVersion(current, input.expectedVersion);
|
|
7310
|
+
yield* ensureMediaAvailable(transaction, input.settings);
|
|
7311
|
+
const revision = yield* insertRevision(transaction, input.settings, input.activity);
|
|
7312
|
+
yield* transaction.update(seoState).set({ workingRevisionId: revision.revisionId }).where(eq(seoState.singleton, true));
|
|
7313
|
+
yield* replaceMediaReferences(transaction, "working", input.settings);
|
|
7314
|
+
yield* recordActivity(transaction, "seo.saved", input.activity, {
|
|
7315
|
+
previousVersion: current.working.version,
|
|
7316
|
+
version: revision.version
|
|
7317
|
+
}, ActivityId.make(revision.revisionId));
|
|
7318
|
+
return {
|
|
7319
|
+
...current,
|
|
7320
|
+
working: revision
|
|
7321
|
+
};
|
|
7322
|
+
})).pipe(mapStoreFailure$1("SeoStore.Drizzle.save")));
|
|
7323
|
+
const publish = Effect.fn("SeoStore.Drizzle.publish")((input) => database.transaction((transaction) => Effect.gen(function* () {
|
|
7324
|
+
const current = yield* lockedSnapshot(transaction);
|
|
7325
|
+
yield* requireVersion(current, input.expectedVersion);
|
|
7326
|
+
yield* ensureMediaAvailable(transaction, current.working.settings);
|
|
7327
|
+
yield* transaction.update(seoState).set({ publishedRevisionId: current.working.revisionId }).where(eq(seoState.singleton, true));
|
|
7328
|
+
yield* replaceMediaReferences(transaction, "published", current.working.settings);
|
|
7329
|
+
yield* recordActivity(transaction, "seo.published", input.activity, { version: current.working.version });
|
|
7330
|
+
return {
|
|
7331
|
+
...current,
|
|
7332
|
+
published: current.working
|
|
7333
|
+
};
|
|
7334
|
+
})).pipe(mapStoreFailure$1("SeoStore.Drizzle.publish")));
|
|
7335
|
+
return SeoStore.of({
|
|
7336
|
+
initialize,
|
|
7337
|
+
load,
|
|
7338
|
+
publish,
|
|
7339
|
+
save
|
|
7340
|
+
});
|
|
8456
7341
|
}));
|
|
7342
|
+
const readSnapshot = Effect.fn("SeoStore.Drizzle.readSnapshot")(function* (database) {
|
|
7343
|
+
const state = (yield* database.select().from(seoState).limit(1))[0];
|
|
7344
|
+
if (!state) return void 0;
|
|
7345
|
+
const revisionRows = yield* database.select().from(seoRevisions).where(inArray(seoRevisions.id, [state.workingRevisionId, state.publishedRevisionId]));
|
|
7346
|
+
const revisions = yield* Effect.forEach(revisionRows, decodeRevision);
|
|
7347
|
+
const working = revisions.find((revision) => revision.revisionId === state.workingRevisionId);
|
|
7348
|
+
const published = revisions.find((revision) => revision.revisionId === state.publishedRevisionId);
|
|
7349
|
+
if (!working || !published) return yield* persistenceFailure$1("SeoStore.Drizzle.readSnapshot", /* @__PURE__ */ new Error("SEO state points to a missing revision."));
|
|
7350
|
+
return {
|
|
7351
|
+
published,
|
|
7352
|
+
working
|
|
7353
|
+
};
|
|
7354
|
+
});
|
|
7355
|
+
const lockedSnapshot = Effect.fn("SeoStore.Drizzle.lockSnapshot")(function* (transaction) {
|
|
7356
|
+
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."));
|
|
7357
|
+
return (yield* readSnapshot(transaction)) ?? (yield* persistenceFailure$1("SeoStore.Drizzle.lockSnapshot", /* @__PURE__ */ new Error("SEO state disappeared.")));
|
|
7358
|
+
});
|
|
7359
|
+
const insertRevision = Effect.fn("SeoStore.Drizzle.insertRevision")(function* (transaction, settings, activity) {
|
|
7360
|
+
const row = (yield* transaction.insert(seoRevisions).values({
|
|
7361
|
+
actorEmail: activity.actor.email,
|
|
7362
|
+
actorId: activity.actor.id,
|
|
7363
|
+
actorName: activity.actor.name,
|
|
7364
|
+
actorRoles: activity.actor.roles,
|
|
7365
|
+
reason: activity.reason,
|
|
7366
|
+
requestId: activity.requestId,
|
|
7367
|
+
settings
|
|
7368
|
+
}).returning())[0];
|
|
7369
|
+
return row ? yield* decodeRevision(row) : yield* persistenceFailure$1("SeoStore.Drizzle.insertRevision", /* @__PURE__ */ new Error("No revision row returned."));
|
|
7370
|
+
});
|
|
7371
|
+
const decodeRevision = Effect.fn("SeoStore.Drizzle.decodeRevision")(function* (row) {
|
|
7372
|
+
return yield* Schema.decodeEffect(SeoRevision)({
|
|
7373
|
+
editedAt: row.createdAt,
|
|
7374
|
+
editedBy: {
|
|
7375
|
+
email: row.actorEmail,
|
|
7376
|
+
id: row.actorId,
|
|
7377
|
+
name: row.actorName,
|
|
7378
|
+
roles: row.actorRoles
|
|
7379
|
+
},
|
|
7380
|
+
revisionId: row.id,
|
|
7381
|
+
settings: row.settings,
|
|
7382
|
+
version: row.id
|
|
7383
|
+
}).pipe(Effect.mapError((cause) => new SeoPersistenceError({
|
|
7384
|
+
cause,
|
|
7385
|
+
operation: "SeoStore.Drizzle.decodeRevision"
|
|
7386
|
+
})));
|
|
7387
|
+
});
|
|
7388
|
+
function requireVersion(snapshot, expectedVersion) {
|
|
7389
|
+
return snapshot.working.version === expectedVersion ? Effect.void : Effect.fail(new SeoVersionConflict({
|
|
7390
|
+
actualVersion: snapshot.working.version,
|
|
7391
|
+
expectedVersion
|
|
7392
|
+
}));
|
|
7393
|
+
}
|
|
7394
|
+
const ensureMediaAvailable = Effect.fn("SeoStore.Drizzle.ensureMediaAvailable")(function* (transaction, settings) {
|
|
7395
|
+
const mediaIds = settingsMediaIds(settings);
|
|
7396
|
+
if (mediaIds.length === 0) return;
|
|
7397
|
+
const initialRows = yield* transaction.select({
|
|
7398
|
+
id: mediaAssets.id,
|
|
7399
|
+
objectKey: mediaAssets.objectKey
|
|
7400
|
+
}).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
|
|
7401
|
+
const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(initialRows.map((row) => row.objectKey));
|
|
7402
|
+
yield* lockMediaReferenceKeys(transaction, objectKeys);
|
|
7403
|
+
const readyRows = yield* transaction.select({
|
|
7404
|
+
id: mediaAssets.id,
|
|
7405
|
+
objectKey: mediaAssets.objectKey
|
|
7406
|
+
}).from(mediaAssets).where(and(inArray(mediaAssets.id, mediaIds), eq(mediaAssets.status, "ready")));
|
|
7407
|
+
const unavailable = settingsMediaImages(settings).find((image) => {
|
|
7408
|
+
const asset = readyRows.find((row) => row.id === image.mediaId);
|
|
7409
|
+
return !asset || mediaObjectKey(image.url) !== asset.objectKey;
|
|
7410
|
+
});
|
|
7411
|
+
if (unavailable) return yield* new SeoMediaUnavailable({ mediaId: unavailable.mediaId });
|
|
7412
|
+
});
|
|
7413
|
+
const replaceMediaReferences = Effect.fn("SeoStore.Drizzle.replaceMediaReferences")(function* (transaction, scope, settings) {
|
|
7414
|
+
yield* transaction.delete(seoMediaReferences).where(eq(seoMediaReferences.scope, scope));
|
|
7415
|
+
const mediaIds = settingsMediaIds(settings);
|
|
7416
|
+
if (mediaIds.length > 0) yield* transaction.insert(seoMediaReferences).values(mediaIds.map((mediaId) => ({
|
|
7417
|
+
mediaId,
|
|
7418
|
+
scope
|
|
7419
|
+
})));
|
|
7420
|
+
});
|
|
7421
|
+
function settingsMediaIds(settings) {
|
|
7422
|
+
return [...new Set(settingsMediaImages(settings).map((image) => image.mediaId))];
|
|
7423
|
+
}
|
|
7424
|
+
function settingsMediaImages(settings) {
|
|
7425
|
+
const images = [settings.socialImage];
|
|
7426
|
+
const identity = settings.identity;
|
|
7427
|
+
if (identity?.type === "Person") images.push(identity.image);
|
|
7428
|
+
if (identity?.type === "Organization") images.push(identity.logo);
|
|
7429
|
+
if (identity?.type === "LocalBusiness") images.push(identity.logo, identity.image);
|
|
7430
|
+
return images.filter((image) => image !== void 0);
|
|
7431
|
+
}
|
|
7432
|
+
function mediaObjectKey(url) {
|
|
7433
|
+
try {
|
|
7434
|
+
const encodedKey = new URL(url).pathname.match(/(?:^|\/)(media\/[^/]+)$/u)?.[1];
|
|
7435
|
+
return encodedKey ? encodedKey.split("/").map(decodeURIComponent).join("/") : void 0;
|
|
7436
|
+
} catch {
|
|
7437
|
+
return;
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
const recordActivity = Effect.fn("SeoStore.Drizzle.recordActivity")(function* (transaction, action, context, details, id) {
|
|
7441
|
+
yield* transaction.insert(activityEntries).values({
|
|
7442
|
+
action,
|
|
7443
|
+
actorEmail: context.actor.email,
|
|
7444
|
+
actorId: context.actor.id,
|
|
7445
|
+
actorName: context.actor.name,
|
|
7446
|
+
actorRoles: context.actor.roles,
|
|
7447
|
+
details,
|
|
7448
|
+
id,
|
|
7449
|
+
reason: context.reason,
|
|
7450
|
+
requestId: context.requestId,
|
|
7451
|
+
targetKind: "seo"
|
|
7452
|
+
});
|
|
7453
|
+
});
|
|
7454
|
+
function mapStoreFailure$1(operation) {
|
|
7455
|
+
return (effect) => effect.pipe(Effect.mapError((cause) => isSeoStoreDomainError(cause) ? cause : new SeoPersistenceError({
|
|
7456
|
+
cause,
|
|
7457
|
+
operation
|
|
7458
|
+
})));
|
|
7459
|
+
}
|
|
7460
|
+
function mapPersistenceFailure$1(operation) {
|
|
7461
|
+
return (effect) => effect.pipe(Effect.mapError((cause) => isSeoPersistenceError(cause) ? cause : new SeoPersistenceError({
|
|
7462
|
+
cause,
|
|
7463
|
+
operation
|
|
7464
|
+
})));
|
|
7465
|
+
}
|
|
7466
|
+
function persistenceFailure$1(operation, cause) {
|
|
7467
|
+
return Effect.fail(new SeoPersistenceError({
|
|
7468
|
+
cause,
|
|
7469
|
+
operation
|
|
7470
|
+
}));
|
|
7471
|
+
}
|
|
8457
7472
|
|
|
8458
7473
|
//#endregion
|
|
8459
7474
|
//#region ../sql-pg/src/internal/user-administration/user-command-fence.ts
|
|
@@ -8471,7 +7486,7 @@ const postgresUserCommandFenceLayer = Layer.effect(UserCommandFence, PostgresAdv
|
|
|
8471
7486
|
const WORKING_DOCUMENT_CHANGES_CHANNEL = "cms_working_documents";
|
|
8472
7487
|
const WorkingDocumentChangePayload = Schema.fromJsonString(WorkingDocumentChange);
|
|
8473
7488
|
function workingDocumentChangePayload(change) {
|
|
8474
|
-
return Schema.
|
|
7489
|
+
return Result.getOrThrow(Schema.encodeResult(WorkingDocumentChangePayload)(change));
|
|
8475
7490
|
}
|
|
8476
7491
|
const decodeWorkingDocumentChangePayload = Schema.decodeUnknownEffect(WorkingDocumentChangePayload);
|
|
8477
7492
|
|
|
@@ -8506,12 +7521,13 @@ const makePostgresChanges = Effect.fn("WorkingDocumentChanges.makePostgres")(fun
|
|
|
8506
7521
|
const postgres = Context.get(context, PgClient.PgClient);
|
|
8507
7522
|
const heartbeats = /* @__PURE__ */ new Map();
|
|
8508
7523
|
let heartbeat = 0;
|
|
8509
|
-
const
|
|
8510
|
-
|
|
7524
|
+
const notifications = yield* postgres.listen(WORKING_DOCUMENT_CHANGES_CHANNEL).pipe(Effect.mapError((cause) => changesError("listen", cause)));
|
|
7525
|
+
const listener = yield* Stream.fromQueue(notifications).pipe(Stream.runForEach((payload) => {
|
|
7526
|
+
const waiting = heartbeats.get(payload.payload);
|
|
8511
7527
|
if (waiting !== void 0) return Deferred.succeed(waiting, void 0).pipe(Effect.asVoid);
|
|
8512
|
-
if (payload.startsWith("__cms_probe__:")) return Effect.void;
|
|
8513
|
-
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))));
|
|
8514
|
-
}), Effect.
|
|
7528
|
+
if (payload.payload.startsWith("__cms_probe__:")) return Effect.void;
|
|
7529
|
+
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))));
|
|
7530
|
+
}), Effect.catchCause((cause) => Effect.fail(changesError("listen", cause)))).pipe(Effect.forkScoped({ startImmediately: true }));
|
|
8515
7531
|
const pulse = Effect.gen(function* () {
|
|
8516
7532
|
heartbeat += 1;
|
|
8517
7533
|
const payload = `__cms_probe__:${session}:${heartbeat}`;
|
|
@@ -9069,14 +8085,11 @@ function postgresLayer(options) {
|
|
|
9069
8085
|
});
|
|
9070
8086
|
const migrations = cmsDatabaseMigrationLayer.pipe(Layer.provide(database));
|
|
9071
8087
|
const locks = postgresAdvisoryLockLayer(options);
|
|
9072
|
-
|
|
9073
|
-
const readiness = postgresReadinessLayer.pipe(Layer.provide(stores), Layer.provide(database));
|
|
9074
|
-
return Layer.merge(stores, readiness);
|
|
8088
|
+
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));
|
|
9075
8089
|
}
|
|
9076
8090
|
|
|
9077
8091
|
//#endregion
|
|
9078
8092
|
//#region src/runtime/installation.ts
|
|
9079
|
-
/** Composes the private providers selected by the Nuxt product adapter. */
|
|
9080
8093
|
const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options) {
|
|
9081
8094
|
const crypto = NodeCrypto.layer;
|
|
9082
8095
|
const postgres = postgresLayer({ url: Redacted.make(options.databaseUrl) });
|
|
@@ -9088,6 +8101,8 @@ const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options)
|
|
|
9088
8101
|
const core = Layer.mergeAll(crypto, postgres, identity);
|
|
9089
8102
|
return yield* installCms$1({
|
|
9090
8103
|
baseUrl: options.baseUrl,
|
|
8104
|
+
formHandlers: options.formHandlers,
|
|
8105
|
+
authoring: options.authoring,
|
|
9091
8106
|
bootstrap: options.bootstrap,
|
|
9092
8107
|
cleanup: options.cleanup.enabled ? {
|
|
9093
8108
|
initialDelayMillis: options.cleanup.initialDelayMillis,
|
|
@@ -9125,4 +8140,4 @@ function toS3MediaObjectStoreOptions(options) {
|
|
|
9125
8140
|
|
|
9126
8141
|
//#endregion
|
|
9127
8142
|
export { toS3MediaObjectStoreOptions as n, installCms as t };
|
|
9128
|
-
//# sourceMappingURL=installation-
|
|
8143
|
+
//# sourceMappingURL=installation-DgF2M1gL.js.map
|