@phtngyn/cms 0.1.0-beta.1 → 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 +192 -9
- package/package.json +30 -15
- package/src/Configuration-Br7tE9Qt.d.ts +68 -0
- package/src/{Configuration-DyKo0sLS.js → Configuration-CA_jvQKG.js} +26 -147
- 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-DYObwMDY.js → DocumentEngine-CgiBQU4g.js} +75 -27
- package/src/DocumentEngine-CgiBQU4g.js.map +1 -0
- package/src/DocumentModel-BB9igGC7.d.ts +34 -0
- package/src/DocumentModel.d.ts +2 -34
- 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-BbS21Jy8.js → HttpPaths-c9nLZ9H2.js} +6 -2
- 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 +57 -11
- 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-CBTY-TzB.js → connection-B-iz0Y9Y.js} +5 -7
- package/src/connection-B-iz0Y9Y.js.map +1 -0
- package/src/{installation-DeuBiFRE.js → installation-DgF2M1gL.js} +1005 -2242
- package/src/installation-DgF2M1gL.js.map +1 -0
- package/src/internal/authoring-catalog.js +10 -4
- package/src/internal/authoring-catalog.js.map +1 -1
- package/src/internal/authoring-compiler.js +0 -1
- package/src/internal/authoring-compiler.js.map +1 -1
- package/src/internal/authoring-metadata.js +4 -4
- package/src/internal/authoring-metadata.js.map +1 -1
- package/src/internal/cms-config.js +0 -1
- package/src/internal/cms-config.js.map +1 -1
- package/src/internal/configuration.js +23 -10
- package/src/internal/configuration.js.map +1 -1
- package/src/internal/document-model.js +65 -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 +2 -2
- package/src/internal/studio-app.js.map +1 -1
- package/src/{location-DtYv2aes.js → location-CFKTeK1K.js} +2 -3
- package/src/location-CFKTeK1K.js.map +1 -0
- package/src/{metadata-DE1Mc2td.js → metadata-BEZPWh-3.js} +3 -72
- package/src/metadata-BEZPWh-3.js.map +1 -0
- package/src/{models-C3witQBK.js → models-BWm1pTAf.js} +3 -2
- package/src/models-BWm1pTAf.js.map +1 -0
- package/src/{models-BkMgeaOL.js → models-DwYioyNV.js} +203 -32
- package/src/models-DwYioyNV.js.map +1 -0
- package/src/runtime/blocks.js +22 -9
- package/src/runtime/blocks.js.map +1 -1
- package/src/runtime/collaboration.js.map +1 -1
- package/src/runtime/delivery/DocumentRenderer.js +7 -4
- package/src/runtime/delivery/DocumentRenderer.js.map +1 -1
- package/src/runtime/delivery/blocks.js +3 -1
- package/src/runtime/delivery/blocks.js.map +1 -1
- package/src/runtime/delivery/context.js +1 -1
- package/src/runtime/delivery/document-page.js +12 -12
- package/src/runtime/delivery/document-page.js.map +1 -1
- package/src/runtime/delivery/document.js +4 -2
- package/src/runtime/delivery/document.js.map +1 -1
- package/src/runtime/delivery/error.js.map +1 -1
- package/src/runtime/delivery/plugin.js +22 -2
- package/src/runtime/delivery/plugin.js.map +1 -1
- package/src/runtime/delivery/types.d.ts +5 -0
- package/src/runtime/delivery/useCmsDocumentSeo.d.ts +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 +2 -2
- package/src/runtime/delivery/useDocument.js.map +1 -1
- package/src/runtime/delivery/useDocumentQuery.js +2 -2
- package/src/runtime/delivery/useDocumentQuery.js.map +1 -1
- package/src/runtime/forms/document.js +38 -0
- package/src/runtime/forms/document.js.map +1 -0
- package/src/runtime/forms/useCmsForm.js +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 +1 -1
- package/src/runtime/plugin.js.map +1 -1
- package/src/runtime/preview/comark-document.js +0 -1
- package/src/runtime/preview/comark-document.js.map +1 -1
- package/src/runtime/preview/connection.js +1 -1
- package/src/runtime/preview/controller.js +10 -3
- package/src/runtime/preview/controller.js.map +1 -1
- package/src/runtime/preview/overlay.js.map +1 -1
- package/src/runtime/preview/plugin.client.js +59 -48
- package/src/runtime/preview/plugin.client.js.map +1 -1
- package/src/runtime/preview/protocol.js.map +1 -1
- package/src/runtime/preview/store.js +1 -1
- package/src/runtime/preview/targets.js +0 -1
- package/src/runtime/preview/targets.js.map +1 -1
- package/src/runtime/sitemap-entries.js +17 -0
- package/src/runtime/sitemap-entries.js.map +1 -0
- package/src/runtime/sitemap.js +24 -0
- package/src/runtime/sitemap.js.map +1 -0
- package/src/runtime/studio-app.js.map +1 -1
- package/src/runtime/studio-entry.js +2 -2
- package/src/runtime/studio-entry.js.map +1 -1
- package/src/runtime/studio-navigation.js +1 -1
- package/src/scalars-B2lgGUnj.js +26 -0
- package/src/scalars-B2lgGUnj.js.map +1 -0
- package/src/{store-CfFFx9Ua.js → store-DjJs3I7k.js} +12 -7
- package/src/store-DjJs3I7k.js.map +1 -0
- package/studio/{CreateDocumentDialog--L9lTfx6.js → CreateDocumentDialog-CTeIFlPb.js} +44 -44
- package/studio/{collaboration-DUuu678R.js → collaboration-DcK2F6v9.js} +7 -6
- package/studio/{documentPlanning-BXX04Woe.js → documentPlanning-Bfv-d7OU.js} +7310 -6479
- package/studio/{editor-PexnXkJY.js → editor-Bzlh3zXt.js} +7965 -7955
- package/studio/host.css +1 -1
- package/studio/host.js +10416 -7981
- package/src/Configuration-DyKo0sLS.js.map +0 -1
- package/src/DocumentEngine-DYObwMDY.js.map +0 -1
- package/src/HttpPaths-BbS21Jy8.js.map +0 -1
- package/src/StudioHttp-jR2-bc01.js +0 -718
- package/src/StudioHttp-jR2-bc01.js.map +0 -1
- package/src/connection-CBTY-TzB.js.map +0 -1
- package/src/installation-DeuBiFRE.js.map +0 -1
- package/src/location-DtYv2aes.js.map +0 -1
- package/src/metadata-DE1Mc2td.js.map +0 -1
- package/src/models-BkMgeaOL.js.map +0 -1
- package/src/models-C3witQBK.js.map +0 -1
- package/src/store-CfFFx9Ua.js.map +0 -1
|
@@ -0,0 +1,2664 @@
|
|
|
1
|
+
import { c as RevisionId, d as SourcePath, f as nonNegativeInt, i as DocumentVersion, n as DocumentHistoryCursor, o as PublicPath, r as DocumentId, s as RelativeDocumentPath, t as DocumentGeneration, u as Slug } from "./scalars-B2lgGUnj.js";
|
|
2
|
+
import { i as FormResult, o as FormSubmission } from "./Forms-CfcI3tPU.js";
|
|
3
|
+
import { a as encodeCmsEditorComponentAttributes, d as createCmsComarkRenderData, m as toComarkDocument, p as mutableJsonObject, s as projectCmsEditorComponentAttributes } from "./Forms-DxbkU3_5.js";
|
|
4
|
+
import { $ as DOCUMENT_ACTIONS, A as RemoveDocumentInput, B as SeoCapabilities, Ct as MediaTooLarge, D as PublishedDocumentPage, E as PublishedDocumentLookup, H as SeoConfirmationRequired, I as GlobalSeoSettings, J as SeoUrl, K as SeoSnapshot, L as PublishSeoInput, M as UndeleteDocumentInput, O as PublishedDocumentQuery, P as UpdateDocumentInput, Q as AuthenticationRequired, R as SaveSeoInput, S as DuplicateDocumentInput, T as PublishDocumentInput, U as SeoMediaUnavailable, V as SeoConfiguration, W as SeoMutationResult, X as SitemapEntry, Y as SeoVersionConflict, Z as AccessDenied, _ as DocumentSource, _t as MediaObjectKey, at as ImageMimeType, b as DocumentSurround, bt as MediaReference, ct as MediaCleanupReport, d as DocumentIssue, dt as MediaFolder, et as MEDIA_ACTIONS, f as DocumentMutationResult, ft as MediaId, g as DocumentRevisionAction, gt as MediaNotFound, h as DocumentQueryScalar, i as CreateDocumentInput, j as RestoreDocumentInput, k as PublishedDocumentSurroundQuery, kt as RequestId, l as DocumentImportReport, lt as MediaConflict, m as DocumentNavigationQuery, mt as MediaInvalid, n as CmsMarkdownDocument, nt as StudioActor, o as DeletedDocument, ot as MediaAsset, p as DocumentNavigationItem, pt as MediaIntegrityReport, q as SeoState, r as CompiledDocument, rt as StudioEntryDenied, s as DocumentBrowseQuery, st as MediaByteSize, tt as Role, vt as MediaPage, w as MoveDocumentInput, wt as MediaUpload, x as DocumentValidationResult, xt as MediaReferenced, y as DocumentSummary, yt as MediaQuery, z as SeoCanonicalOrigin } from "./models-DwYioyNV.js";
|
|
5
|
+
import { u as STUDIO_API_PATH } from "./HttpPaths-c9nLZ9H2.js";
|
|
6
|
+
import { i as withoutEditorMetadata } from "./metadata-BEZPWh-3.js";
|
|
7
|
+
import { d as EditorRenderFailed, i as MediaConfiguration, r as DocumentConfiguration } from "./Configuration-CA_jvQKG.js";
|
|
8
|
+
import { a as normalizeDocumentRelativePath, c as resolveDocumentAddress, i as localizeDocumentPath, n as referenceTarget, o as normalizePublicPath, r as DocumentCompilationFailed, s as parseDocumentAddress, t as createDocumentEngine } from "./DocumentEngine-CgiBQU4g.js";
|
|
9
|
+
import { Context, Crypto, Data, Effect, Encoding, Layer, Option, Predicate, PubSub, Redacted, Ref, Result, Schedule, Schema, Semaphore, Stream, flow } from "effect";
|
|
10
|
+
import { NodeStream } from "@effect/platform-node-shared";
|
|
11
|
+
import { Multipart } from "effect/unstable/http";
|
|
12
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema } from "effect/unstable/httpapi";
|
|
13
|
+
import { renderMarkdown } from "comark/render";
|
|
14
|
+
import { diffArrays } from "diff";
|
|
15
|
+
import { CopyObjectCommand, CreateBucketCommand, DeleteObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
16
|
+
|
|
17
|
+
//#region ../protocol/src/internal/documents/errors.ts
|
|
18
|
+
var DocumentNotFound = class extends Schema.TaggedError()("Document.NotFound", {
|
|
19
|
+
documentId: DocumentId,
|
|
20
|
+
revisionId: Schema.optionalKey(RevisionId)
|
|
21
|
+
}) {};
|
|
22
|
+
var DocumentPathConflict = class extends Schema.TaggedError()("Document.PathConflict", { sourcePath: SourcePath }) {};
|
|
23
|
+
var DocumentVersionConflict = class extends Schema.TaggedError()("Document.VersionConflict", {
|
|
24
|
+
documentId: DocumentId,
|
|
25
|
+
expectedVersion: DocumentVersion,
|
|
26
|
+
actualVersion: DocumentVersion
|
|
27
|
+
}) {};
|
|
28
|
+
var DocumentMediaUnavailable = class extends Schema.TaggedError()("Document.MediaUnavailable", {
|
|
29
|
+
documentId: DocumentId,
|
|
30
|
+
mediaKeys: Schema.Array(MediaObjectKey).check(Schema.isMinLength(1))
|
|
31
|
+
}) {};
|
|
32
|
+
var DocumentValidationFailed = class extends Schema.TaggedError()("Document.ValidationFailed", { issues: Schema.Array(DocumentIssue) }) {};
|
|
33
|
+
var PublishedDocumentNotFound = class extends Schema.TaggedError()("PublishedDocument.NotFound", { lookup: PublishedDocumentLookup }) {};
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ../protocol/src/Activity.ts
|
|
37
|
+
/**
|
|
38
|
+
* Immutable completed Activity facts. Pages are ordered newest-first; cursors are opaque,
|
|
39
|
+
* stable continuation tokens and an invalid cursor is a caller-visible failure.
|
|
40
|
+
*/
|
|
41
|
+
const ActivityId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("ActivityId"));
|
|
42
|
+
const ActivityCursor = Schema.NonEmptyString.pipe(Schema.brand("ActivityCursor"));
|
|
43
|
+
const ACTIVITY_ACTIONS = [
|
|
44
|
+
"document.created",
|
|
45
|
+
"document.updated",
|
|
46
|
+
"document.moved",
|
|
47
|
+
"document.trashed",
|
|
48
|
+
"document.revision.restored",
|
|
49
|
+
"document.untrashed",
|
|
50
|
+
"document.published",
|
|
51
|
+
"document.unpublished",
|
|
52
|
+
"media.uploaded",
|
|
53
|
+
"media.replaced",
|
|
54
|
+
"media.removed",
|
|
55
|
+
"media.cleaned",
|
|
56
|
+
"seo.saved",
|
|
57
|
+
"seo.published",
|
|
58
|
+
"user.created",
|
|
59
|
+
"user.removed",
|
|
60
|
+
"user.role.changed",
|
|
61
|
+
"user.sessions.revoked",
|
|
62
|
+
"authentication.signed-in",
|
|
63
|
+
"authentication.signed-out",
|
|
64
|
+
"authentication.password.changed",
|
|
65
|
+
"authentication.password.reset"
|
|
66
|
+
];
|
|
67
|
+
const ActivityAction = Schema.Literals(ACTIVITY_ACTIONS);
|
|
68
|
+
const DocumentActivityTarget = Schema.Struct({
|
|
69
|
+
kind: Schema.tag("document"),
|
|
70
|
+
documentId: DocumentId
|
|
71
|
+
});
|
|
72
|
+
const MediaActivityTarget = Schema.Struct({
|
|
73
|
+
kind: Schema.tag("media"),
|
|
74
|
+
mediaId: MediaId
|
|
75
|
+
});
|
|
76
|
+
const MediaLibraryActivityTarget = Schema.Struct({ kind: Schema.tag("media-library") });
|
|
77
|
+
const ActivityTarget = Schema.Union([
|
|
78
|
+
DocumentActivityTarget,
|
|
79
|
+
MediaActivityTarget,
|
|
80
|
+
MediaLibraryActivityTarget,
|
|
81
|
+
Schema.Struct({ kind: Schema.tag("seo") }),
|
|
82
|
+
Schema.Struct({
|
|
83
|
+
kind: Schema.tag("user"),
|
|
84
|
+
userId: Schema.NonEmptyString
|
|
85
|
+
})
|
|
86
|
+
]).pipe(Schema.toTaggedUnion("kind"));
|
|
87
|
+
const ActivityTargetKind = Schema.Literals([
|
|
88
|
+
"document",
|
|
89
|
+
"media",
|
|
90
|
+
"media-library",
|
|
91
|
+
"seo",
|
|
92
|
+
"user"
|
|
93
|
+
]);
|
|
94
|
+
const ActivityContext = Schema.Struct({
|
|
95
|
+
actor: StudioActor,
|
|
96
|
+
requestId: RequestId,
|
|
97
|
+
reason: Schema.optionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(1e3)))
|
|
98
|
+
}).annotate({ identifier: "ActivityContext" });
|
|
99
|
+
const ActivityEntry = Schema.Struct({
|
|
100
|
+
id: ActivityId,
|
|
101
|
+
action: ActivityAction,
|
|
102
|
+
actor: StudioActor,
|
|
103
|
+
requestId: RequestId,
|
|
104
|
+
reason: ActivityContext.fields.reason,
|
|
105
|
+
target: ActivityTarget,
|
|
106
|
+
details: Schema.JsonObject,
|
|
107
|
+
occurredAt: Schema.DateTimeUtcFromString
|
|
108
|
+
}).annotate({ identifier: "ActivityEntry" });
|
|
109
|
+
const ActivityEntryDraft = Schema.Struct({
|
|
110
|
+
action: ActivityAction,
|
|
111
|
+
context: ActivityContext,
|
|
112
|
+
target: ActivityTarget,
|
|
113
|
+
details: Schema.JsonObject
|
|
114
|
+
}).annotate({ identifier: "ActivityEntryDraft" });
|
|
115
|
+
const ActivityQuery = Schema.Struct({
|
|
116
|
+
action: Schema.optionalKey(ActivityAction),
|
|
117
|
+
actorId: Schema.optionalKey(StudioActor.fields.id),
|
|
118
|
+
cursor: Schema.optionalKey(ActivityCursor),
|
|
119
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({
|
|
120
|
+
minimum: 1,
|
|
121
|
+
maximum: 100
|
|
122
|
+
}))),
|
|
123
|
+
requestId: Schema.optionalKey(RequestId),
|
|
124
|
+
targetId: Schema.optionalKey(Schema.NonEmptyString),
|
|
125
|
+
targetKind: Schema.optionalKey(ActivityTargetKind)
|
|
126
|
+
}).annotate({ identifier: "ActivityQuery" });
|
|
127
|
+
const ActivityPage = Schema.Struct({
|
|
128
|
+
entries: Schema.Array(ActivityEntry),
|
|
129
|
+
nextCursor: Schema.optionalKey(ActivityCursor),
|
|
130
|
+
total: nonNegativeInt
|
|
131
|
+
}).annotate({ identifier: "ActivityPage" });
|
|
132
|
+
var ActivityCursorInvalid = class extends Schema.TaggedError()("Activity.CursorInvalid", {}) {};
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region ../protocol/src/internal/documents/history.ts
|
|
136
|
+
const DocumentRevisionSummary = Schema.Struct({
|
|
137
|
+
action: DocumentRevisionAction,
|
|
138
|
+
activity: ActivityEntry,
|
|
139
|
+
createdAt: Schema.DateTimeUtcFromString,
|
|
140
|
+
documentId: DocumentId,
|
|
141
|
+
revisionId: RevisionId,
|
|
142
|
+
sourcePath: SourcePath,
|
|
143
|
+
version: DocumentVersion
|
|
144
|
+
}).annotate({ identifier: "DocumentRevisionSummary" });
|
|
145
|
+
const DocumentRevision = Schema.Struct({
|
|
146
|
+
...DocumentRevisionSummary.fields,
|
|
147
|
+
markdown: Schema.String
|
|
148
|
+
}).annotate({ identifier: "DocumentRevision" });
|
|
149
|
+
const DocumentHistoryQuery = Schema.Struct({
|
|
150
|
+
documentId: DocumentId,
|
|
151
|
+
cursor: Schema.optionalKey(DocumentHistoryCursor),
|
|
152
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({
|
|
153
|
+
minimum: 1,
|
|
154
|
+
maximum: 100
|
|
155
|
+
})))
|
|
156
|
+
}).annotate({ identifier: "DocumentHistoryQuery" });
|
|
157
|
+
const DocumentRevisionPage = Schema.Struct({
|
|
158
|
+
revisions: Schema.Array(DocumentRevisionSummary),
|
|
159
|
+
nextCursor: Schema.optionalKey(DocumentHistoryCursor),
|
|
160
|
+
total: nonNegativeInt
|
|
161
|
+
}).annotate({ identifier: "DocumentRevisionPage" });
|
|
162
|
+
const LineNumber = Schema.Int.check(Schema.isGreaterThan(0));
|
|
163
|
+
const DocumentRevisionChange = Schema.Union([
|
|
164
|
+
Schema.Struct({
|
|
165
|
+
kind: Schema.tag("equal"),
|
|
166
|
+
beforeLine: LineNumber,
|
|
167
|
+
afterLine: LineNumber,
|
|
168
|
+
line: Schema.String
|
|
169
|
+
}),
|
|
170
|
+
Schema.Struct({
|
|
171
|
+
kind: Schema.tag("added"),
|
|
172
|
+
afterLine: LineNumber,
|
|
173
|
+
line: Schema.String
|
|
174
|
+
}),
|
|
175
|
+
Schema.Struct({
|
|
176
|
+
kind: Schema.tag("removed"),
|
|
177
|
+
beforeLine: LineNumber,
|
|
178
|
+
line: Schema.String
|
|
179
|
+
})
|
|
180
|
+
]).pipe(Schema.toTaggedUnion("kind"));
|
|
181
|
+
const CompareDocumentRevisionsInput = Schema.Struct({
|
|
182
|
+
documentId: DocumentId,
|
|
183
|
+
fromRevisionId: RevisionId,
|
|
184
|
+
toRevisionId: RevisionId
|
|
185
|
+
}).annotate({ identifier: "CompareDocumentRevisionsInput" });
|
|
186
|
+
const DocumentRevisionComparison = Schema.Struct({
|
|
187
|
+
from: DocumentRevision,
|
|
188
|
+
to: DocumentRevision,
|
|
189
|
+
changes: Schema.Array(DocumentRevisionChange)
|
|
190
|
+
}).annotate({ identifier: "DocumentRevisionComparison" });
|
|
191
|
+
var DocumentHistoryInvalidCursor = class extends Schema.TaggedError()("DocumentHistory.InvalidCursor", {}) {};
|
|
192
|
+
var DocumentHistoryComparisonTooLarge = class extends Schema.TaggedError()("DocumentHistory.ComparisonTooLarge", {
|
|
193
|
+
maximumEditLength: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
194
|
+
maximumLines: Schema.Int.check(Schema.isGreaterThan(0))
|
|
195
|
+
}) {};
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region ../protocol/src/Collaboration.ts
|
|
199
|
+
/**
|
|
200
|
+
* Collaboration handshake and WebSocket envelopes. Command results correlate by RequestId;
|
|
201
|
+
* checkpoints carry the durable state vector needed for retry and recovery.
|
|
202
|
+
*/
|
|
203
|
+
const StateVector = Schema.String.check(Schema.isBase64()).pipe(Schema.brand("StateVector"));
|
|
204
|
+
const HttpErrorStatus = Schema.Int.check(Schema.isBetween({
|
|
205
|
+
minimum: 400,
|
|
206
|
+
maximum: 599
|
|
207
|
+
}));
|
|
208
|
+
const CollaborationUser = Schema.Struct({
|
|
209
|
+
color: Schema.NonEmptyString,
|
|
210
|
+
id: Schema.NonEmptyString,
|
|
211
|
+
name: Schema.NonEmptyString
|
|
212
|
+
});
|
|
213
|
+
const CollaborationTicket = Schema.Struct({
|
|
214
|
+
authoringRevision: Schema.NonEmptyString,
|
|
215
|
+
authoredStateVector: StateVector,
|
|
216
|
+
documentName: Schema.NonEmptyString,
|
|
217
|
+
source: DocumentSource,
|
|
218
|
+
token: Schema.NonEmptyString,
|
|
219
|
+
url: Schema.NonEmptyString,
|
|
220
|
+
user: CollaborationUser,
|
|
221
|
+
workingStateVector: StateVector
|
|
222
|
+
}).annotate({ identifier: "CollaborationTicket" });
|
|
223
|
+
const CollaborationPreview = DocumentValidationResult.pipe(Schema.fieldsAssign({
|
|
224
|
+
markdown: Schema.String,
|
|
225
|
+
preview: Schema.Struct({
|
|
226
|
+
body: CmsMarkdownDocument,
|
|
227
|
+
fallbacks: Schema.Record(Schema.String, Schema.String)
|
|
228
|
+
})
|
|
229
|
+
})).annotate({ identifier: "CollaborationPreview" });
|
|
230
|
+
const CheckpointMessage = Schema.Struct({ type: Schema.tag("checkpoint") });
|
|
231
|
+
const PreviewMessage = Schema.Struct({
|
|
232
|
+
type: Schema.tag("preview"),
|
|
233
|
+
requestId: RequestId
|
|
234
|
+
});
|
|
235
|
+
const PublishMessage = Schema.Struct({
|
|
236
|
+
type: Schema.tag("publish"),
|
|
237
|
+
requestId: RequestId
|
|
238
|
+
});
|
|
239
|
+
const CollaborationClientMessage = Schema.Union([
|
|
240
|
+
CheckpointMessage,
|
|
241
|
+
PreviewMessage,
|
|
242
|
+
PublishMessage
|
|
243
|
+
]).pipe(Schema.toTaggedUnion("type"));
|
|
244
|
+
const requestFailure = {
|
|
245
|
+
error: Schema.NonEmptyString,
|
|
246
|
+
statusCode: HttpErrorStatus
|
|
247
|
+
};
|
|
248
|
+
const RequestFailure = Schema.Struct({
|
|
249
|
+
status: Schema.tag("failure"),
|
|
250
|
+
...requestFailure
|
|
251
|
+
});
|
|
252
|
+
const PreviewOutcome = Schema.Union([Schema.Struct({
|
|
253
|
+
status: Schema.tag("success"),
|
|
254
|
+
result: CollaborationPreview
|
|
255
|
+
}), RequestFailure]).pipe(Schema.toTaggedUnion("status"));
|
|
256
|
+
const PublishCheckpoint = Schema.Struct({
|
|
257
|
+
authoredStateVector: StateVector,
|
|
258
|
+
result: Schema.optionalKey(DocumentMutationResult)
|
|
259
|
+
});
|
|
260
|
+
const PublishOutcome = Schema.Union([Schema.Struct({
|
|
261
|
+
status: Schema.tag("success"),
|
|
262
|
+
authoredStateVector: StateVector,
|
|
263
|
+
result: DocumentMutationResult
|
|
264
|
+
}), Schema.Struct({
|
|
265
|
+
status: Schema.tag("failure"),
|
|
266
|
+
...requestFailure,
|
|
267
|
+
checkpoint: Schema.optionalKey(PublishCheckpoint)
|
|
268
|
+
})]).pipe(Schema.toTaggedUnion("status"));
|
|
269
|
+
const WorkingSavedMessage = Schema.Struct({
|
|
270
|
+
type: Schema.tag("working-saved"),
|
|
271
|
+
workingStateVector: StateVector
|
|
272
|
+
});
|
|
273
|
+
const WorkingSaveErrorMessage = Schema.Struct({
|
|
274
|
+
type: Schema.tag("working-save-error"),
|
|
275
|
+
error: Schema.NonEmptyString
|
|
276
|
+
});
|
|
277
|
+
const PreviewResultMessage = Schema.Struct({
|
|
278
|
+
type: Schema.tag("preview-result"),
|
|
279
|
+
requestId: RequestId,
|
|
280
|
+
outcome: PreviewOutcome
|
|
281
|
+
});
|
|
282
|
+
const PublishResultMessage = Schema.Struct({
|
|
283
|
+
type: Schema.tag("publish-result"),
|
|
284
|
+
requestId: RequestId,
|
|
285
|
+
outcome: PublishOutcome
|
|
286
|
+
});
|
|
287
|
+
const CollaborationServerMessage = Schema.Union([
|
|
288
|
+
WorkingSavedMessage,
|
|
289
|
+
WorkingSaveErrorMessage,
|
|
290
|
+
PreviewResultMessage,
|
|
291
|
+
PublishResultMessage
|
|
292
|
+
]).pipe(Schema.toTaggedUnion("type"));
|
|
293
|
+
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region ../protocol/src/Users.ts
|
|
296
|
+
/**
|
|
297
|
+
* User administration inputs, offset-paginated directory results, and caller-visible command
|
|
298
|
+
* failures. Commands carry no idempotency key; clients resolve ambiguous outcomes by listing again.
|
|
299
|
+
*/
|
|
300
|
+
const UserEmail = Schema.NonEmptyString.check(Schema.isMaxLength(320), Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/u)).annotate({ identifier: "UserEmail" });
|
|
301
|
+
const UserName = Schema.NonEmptyString.check(Schema.isMaxLength(200)).annotate({ identifier: "UserName" });
|
|
302
|
+
const UserPassword = Schema.NonEmptyString.check(Schema.isMinLength(8), Schema.isMaxLength(200)).annotate({ identifier: "UserPassword" });
|
|
303
|
+
const User = Schema.Struct({
|
|
304
|
+
id: Schema.NonEmptyString,
|
|
305
|
+
email: UserEmail,
|
|
306
|
+
name: UserName,
|
|
307
|
+
role: Role,
|
|
308
|
+
emailVerified: Schema.Boolean,
|
|
309
|
+
banned: Schema.Boolean,
|
|
310
|
+
createdAt: Schema.DateTimeUtcFromString,
|
|
311
|
+
updatedAt: Schema.DateTimeUtcFromString
|
|
312
|
+
}).annotate({ identifier: "User" });
|
|
313
|
+
const UserPage = Schema.Struct({
|
|
314
|
+
users: Schema.Array(User),
|
|
315
|
+
total: nonNegativeInt
|
|
316
|
+
}).annotate({ identifier: "UserPage" });
|
|
317
|
+
const UserQuery = Schema.Struct({
|
|
318
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({
|
|
319
|
+
minimum: 1,
|
|
320
|
+
maximum: 100
|
|
321
|
+
}))),
|
|
322
|
+
offset: Schema.optionalKey(nonNegativeInt),
|
|
323
|
+
search: Schema.optionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(200)))
|
|
324
|
+
}).annotate({ identifier: "UserQuery" });
|
|
325
|
+
const CreateUserInput = Schema.Struct({
|
|
326
|
+
email: UserEmail,
|
|
327
|
+
name: UserName,
|
|
328
|
+
password: UserPassword,
|
|
329
|
+
role: Role
|
|
330
|
+
}).annotate({ identifier: "CreateUserInput" });
|
|
331
|
+
var UserAlreadyExists = class extends Schema.TaggedError()("User.AlreadyExists", { email: UserEmail }) {};
|
|
332
|
+
var UserNotFound = class extends Schema.TaggedError()("User.NotFound", { userId: User.fields.id }) {};
|
|
333
|
+
var UserSelfRemovalDenied = class extends Schema.TaggedError()("User.SelfRemovalDenied", { userId: User.fields.id }) {};
|
|
334
|
+
var UserSelfRoleChangeDenied = class extends Schema.TaggedError()("User.SelfRoleChangeDenied", { userId: User.fields.id }) {};
|
|
335
|
+
var UserAdministrationDenied = class extends Schema.TaggedError()("UserAdministration.Denied", {}) {};
|
|
336
|
+
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region ../protocol/src/Initialization.ts
|
|
339
|
+
const InitializationInput = Schema.Struct({
|
|
340
|
+
email: UserEmail,
|
|
341
|
+
initializationToken: Schema.NonEmptyString.check(Schema.isMaxLength(512)),
|
|
342
|
+
name: UserName,
|
|
343
|
+
password: UserPassword
|
|
344
|
+
}).annotate({ identifier: "InitializationInput" });
|
|
345
|
+
const InitializationState = Schema.Struct({ needsAdministrator: Schema.Boolean }).annotate({ identifier: "InitializationState" });
|
|
346
|
+
var InitializationAlreadyCompleted = class extends Schema.TaggedError()("Initialization.AlreadyCompleted", {}) {};
|
|
347
|
+
var InitializationProofInvalid = class extends Schema.TaggedError()("Initialization.ProofInvalid", {}) {};
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region ../protocol/src/internal/http/failures.ts
|
|
351
|
+
var StudioInternalError = class extends Schema.TaggedError()("Studio.InternalError", { message: Schema.Literal("The request could not be completed.") }) {};
|
|
352
|
+
function studioInternalError() {
|
|
353
|
+
return new StudioInternalError({ message: "The request could not be completed." });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region ../protocol/src/internal/http/delivery.ts
|
|
358
|
+
const DeliverySurroundPayload = Schema.Struct({
|
|
359
|
+
lookup: PublishedDocumentLookup,
|
|
360
|
+
query: PublishedDocumentSurroundQuery
|
|
361
|
+
}).annotate({ identifier: "DeliverySurroundPayload" });
|
|
362
|
+
const InternalError$1 = StudioInternalError.pipe(HttpApiSchema.status(500));
|
|
363
|
+
const PublishedNotFound = PublishedDocumentNotFound.pipe(HttpApiSchema.status(404));
|
|
364
|
+
const Forbidden$1 = AccessDenied.pipe(HttpApiSchema.status(403));
|
|
365
|
+
const PublishedDeliveryApi = HttpApiGroup.make("delivery").add(HttpApiEndpoint.post("read", "/read", {
|
|
366
|
+
payload: PublishedDocumentLookup,
|
|
367
|
+
success: CompiledDocument,
|
|
368
|
+
error: [PublishedNotFound, InternalError$1]
|
|
369
|
+
}), HttpApiEndpoint.post("query", "/query", {
|
|
370
|
+
payload: PublishedDocumentQuery,
|
|
371
|
+
success: PublishedDocumentPage,
|
|
372
|
+
error: InternalError$1
|
|
373
|
+
}), HttpApiEndpoint.post("navigation", "/navigation", {
|
|
374
|
+
payload: DocumentNavigationQuery,
|
|
375
|
+
success: Schema.Array(DocumentNavigationItem),
|
|
376
|
+
error: InternalError$1
|
|
377
|
+
}), HttpApiEndpoint.post("surround", "/surround", {
|
|
378
|
+
payload: DeliverySurroundPayload,
|
|
379
|
+
success: DocumentSurround,
|
|
380
|
+
error: [PublishedNotFound, InternalError$1]
|
|
381
|
+
}), HttpApiEndpoint.get("seo", "/seo", {
|
|
382
|
+
success: SeoSnapshot,
|
|
383
|
+
error: InternalError$1
|
|
384
|
+
}), HttpApiEndpoint.get("sitemap", "/sitemap", {
|
|
385
|
+
success: Schema.Array(SitemapEntry),
|
|
386
|
+
error: InternalError$1
|
|
387
|
+
})).prefix("/delivery");
|
|
388
|
+
const PreviewDeliveryApi = HttpApiGroup.make("previewDelivery").add(HttpApiEndpoint.post("read", "/read", {
|
|
389
|
+
payload: PublishedDocumentLookup,
|
|
390
|
+
success: CompiledDocument,
|
|
391
|
+
error: [
|
|
392
|
+
PublishedNotFound,
|
|
393
|
+
InternalError$1,
|
|
394
|
+
Forbidden$1
|
|
395
|
+
]
|
|
396
|
+
}), HttpApiEndpoint.post("query", "/query", {
|
|
397
|
+
payload: PublishedDocumentQuery,
|
|
398
|
+
success: PublishedDocumentPage,
|
|
399
|
+
error: [InternalError$1, Forbidden$1]
|
|
400
|
+
}), HttpApiEndpoint.post("navigation", "/navigation", {
|
|
401
|
+
payload: DocumentNavigationQuery,
|
|
402
|
+
success: Schema.Array(DocumentNavigationItem),
|
|
403
|
+
error: [InternalError$1, Forbidden$1]
|
|
404
|
+
}), HttpApiEndpoint.post("surround", "/surround", {
|
|
405
|
+
payload: DeliverySurroundPayload,
|
|
406
|
+
success: DocumentSurround,
|
|
407
|
+
error: [
|
|
408
|
+
PublishedNotFound,
|
|
409
|
+
InternalError$1,
|
|
410
|
+
Forbidden$1
|
|
411
|
+
]
|
|
412
|
+
}), HttpApiEndpoint.get("seo", "/seo", {
|
|
413
|
+
success: SeoSnapshot,
|
|
414
|
+
error: [InternalError$1, Forbidden$1]
|
|
415
|
+
})).prefix("/preview");
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region ../protocol/src/StudioHttp.ts
|
|
419
|
+
/**
|
|
420
|
+
* Studio HTTP paths, middleware contracts, payloads, and the composed CMS API. Every expected
|
|
421
|
+
* caller failure is a tagged schema with an explicit status; unclassified failures become the
|
|
422
|
+
* deliberately opaque `StudioInternalError`.
|
|
423
|
+
*/
|
|
424
|
+
const ApiVersion = Schema.NonEmptyString.pipe(Schema.brand("ApiVersion"));
|
|
425
|
+
const StudioBootstrap = Schema.Struct({
|
|
426
|
+
basePath: PublicPath,
|
|
427
|
+
documents: DocumentConfiguration,
|
|
428
|
+
media: MediaConfiguration,
|
|
429
|
+
seo: Schema.optionalKey(SeoConfiguration),
|
|
430
|
+
version: ApiVersion
|
|
431
|
+
}).annotate({ identifier: "StudioBootstrap" });
|
|
432
|
+
var StudioBadRequest = class extends Schema.TaggedError()("Studio.BadRequest", { kind: Schema.Literals([
|
|
433
|
+
"Headers",
|
|
434
|
+
"Params",
|
|
435
|
+
"Payload",
|
|
436
|
+
"Query"
|
|
437
|
+
]) }) {};
|
|
438
|
+
var StudioRequestConflict = class extends Schema.TaggedError()("Studio.RequestConflict", { retryable: Schema.Literal(true) }) {};
|
|
439
|
+
var StudioSchemaError = class extends HttpApiMiddleware.Service()("@internal/protocol/StudioSchemaError", { error: [StudioBadRequest.pipe(HttpApiSchema.status(400)), StudioInternalError.pipe(HttpApiSchema.status(500))] }) {};
|
|
440
|
+
var CurrentStudioRequest = class extends Context.Service()("@internal/protocol/CurrentStudioRequest") {};
|
|
441
|
+
var StudioAuthentication = class extends HttpApiMiddleware.Service()("@internal/protocol/StudioAuthentication", { error: [
|
|
442
|
+
AuthenticationRequired.pipe(HttpApiSchema.status(401)),
|
|
443
|
+
StudioEntryDenied.pipe(HttpApiSchema.status(403)),
|
|
444
|
+
StudioInternalError.pipe(HttpApiSchema.status(500))
|
|
445
|
+
] }) {};
|
|
446
|
+
const ValidateDocumentPayload = Schema.Struct({
|
|
447
|
+
expectedVersion: UpdateDocumentInput.fields.expectedVersion,
|
|
448
|
+
markdown: UpdateDocumentInput.fields.markdown
|
|
449
|
+
});
|
|
450
|
+
const UpdateDocumentPayload = ValidateDocumentPayload.pipe(Schema.fieldsAssign({ reason: UpdateDocumentInput.fields.reason }));
|
|
451
|
+
const MoveDocumentPayload = Schema.Struct({
|
|
452
|
+
expectedVersion: MoveDocumentInput.fields.expectedVersion,
|
|
453
|
+
location: MoveDocumentInput.fields.location,
|
|
454
|
+
reason: MoveDocumentInput.fields.reason
|
|
455
|
+
});
|
|
456
|
+
const PublishDocumentPayload = Schema.Struct({
|
|
457
|
+
expectedVersion: PublishDocumentInput.fields.expectedVersion,
|
|
458
|
+
reason: PublishDocumentInput.fields.reason
|
|
459
|
+
});
|
|
460
|
+
const RemoveDocumentPayload = Schema.Struct({
|
|
461
|
+
expectedVersion: RemoveDocumentInput.fields.expectedVersion,
|
|
462
|
+
reason: RemoveDocumentInput.fields.reason
|
|
463
|
+
});
|
|
464
|
+
const RestoreDocumentPayload = Schema.Struct({
|
|
465
|
+
expectedVersion: RestoreDocumentInput.fields.expectedVersion,
|
|
466
|
+
revisionId: RestoreDocumentInput.fields.revisionId,
|
|
467
|
+
reason: RestoreDocumentInput.fields.reason
|
|
468
|
+
});
|
|
469
|
+
const UndeleteDocumentPayload = Schema.Struct({
|
|
470
|
+
expectedVersion: UndeleteDocumentInput.fields.expectedVersion,
|
|
471
|
+
reason: UndeleteDocumentInput.fields.reason
|
|
472
|
+
});
|
|
473
|
+
const ChangeRolePayload = Schema.Struct({ role: Role }).annotate({ identifier: "ChangeRolePayload" });
|
|
474
|
+
const MediaUploadQuery = Schema.Struct({
|
|
475
|
+
declaredByteSize: MediaUpload.fields.declaredByteSize,
|
|
476
|
+
folder: Schema.optionalKey(MediaFolder)
|
|
477
|
+
}).annotate({ identifier: "MediaUploadQuery" });
|
|
478
|
+
const MediaUploadPayload = Schema.Struct({ file: Multipart.SingleFileSchema }).pipe(HttpApiSchema.asMultipartStream({ maxParts: 1 }));
|
|
479
|
+
const NotFound = DocumentNotFound.pipe(HttpApiSchema.status(404));
|
|
480
|
+
const VersionConflict = DocumentVersionConflict.pipe(HttpApiSchema.status(409));
|
|
481
|
+
const Conflict = Schema.Union([
|
|
482
|
+
DocumentMediaUnavailable,
|
|
483
|
+
DocumentPathConflict,
|
|
484
|
+
DocumentVersionConflict
|
|
485
|
+
]).pipe(HttpApiSchema.status(409));
|
|
486
|
+
const ValidationFailed = DocumentValidationFailed.pipe(HttpApiSchema.status(422));
|
|
487
|
+
const RequestConflict = StudioRequestConflict.pipe(HttpApiSchema.status(409));
|
|
488
|
+
const InternalError = StudioInternalError.pipe(HttpApiSchema.status(500));
|
|
489
|
+
const Forbidden = AccessDenied.pipe(HttpApiSchema.status(403));
|
|
490
|
+
const MutationErrors = [
|
|
491
|
+
NotFound,
|
|
492
|
+
Conflict,
|
|
493
|
+
ValidationFailed,
|
|
494
|
+
RequestConflict,
|
|
495
|
+
Forbidden,
|
|
496
|
+
InternalError
|
|
497
|
+
];
|
|
498
|
+
const MediaNotFoundResponse = MediaNotFound.pipe(HttpApiSchema.status(404));
|
|
499
|
+
const MediaInvalidQuery = MediaInvalid.pipe(HttpApiSchema.status(400));
|
|
500
|
+
const MediaInvalidUpload = MediaInvalid.pipe(HttpApiSchema.status(422));
|
|
501
|
+
const MediaTooLargeResponse = MediaTooLarge.pipe(HttpApiSchema.status(413));
|
|
502
|
+
const MediaReferencedResponse = MediaReferenced.pipe(HttpApiSchema.status(409));
|
|
503
|
+
const MediaConflictResponse = MediaConflict.pipe(HttpApiSchema.status(409));
|
|
504
|
+
const InvalidActivityCursor = ActivityCursorInvalid.pipe(HttpApiSchema.status(400));
|
|
505
|
+
const InvalidHistoryCursor = DocumentHistoryInvalidCursor.pipe(HttpApiSchema.status(400));
|
|
506
|
+
const HistoryComparisonTooLarge = DocumentHistoryComparisonTooLarge.pipe(HttpApiSchema.status(422));
|
|
507
|
+
const AlreadyInitialized = InitializationAlreadyCompleted.pipe(HttpApiSchema.status(409));
|
|
508
|
+
const InitializationProofRejected = InitializationProofInvalid.pipe(HttpApiSchema.status(403));
|
|
509
|
+
const UserAdministrationForbidden = UserAdministrationDenied.pipe(HttpApiSchema.status(403));
|
|
510
|
+
const UserDuplicate = UserAlreadyExists.pipe(HttpApiSchema.status(409));
|
|
511
|
+
const MissingUser = UserNotFound.pipe(HttpApiSchema.status(404));
|
|
512
|
+
const UserSelfRemoval = UserSelfRemovalDenied.pipe(HttpApiSchema.status(409));
|
|
513
|
+
const UserSelfRoleChange = UserSelfRoleChangeDenied.pipe(HttpApiSchema.status(409));
|
|
514
|
+
const SeoConflict = SeoVersionConflict.pipe(HttpApiSchema.status(409));
|
|
515
|
+
const SeoConfirmation = SeoConfirmationRequired.pipe(HttpApiSchema.status(409));
|
|
516
|
+
const SeoMediaInvalid = SeoMediaUnavailable.pipe(HttpApiSchema.status(422));
|
|
517
|
+
const StudioDocumentsApi = HttpApiGroup.make("documents").add(HttpApiEndpoint.get("browse", "/", {
|
|
518
|
+
query: DocumentBrowseQuery,
|
|
519
|
+
success: Schema.Array(DocumentSummary),
|
|
520
|
+
error: [Forbidden, InternalError]
|
|
521
|
+
}), HttpApiEndpoint.get("trash", "/trash", {
|
|
522
|
+
success: Schema.Array(DeletedDocument),
|
|
523
|
+
error: [Forbidden, InternalError]
|
|
524
|
+
}), HttpApiEndpoint.get("read", "/:documentId", {
|
|
525
|
+
params: { documentId: DocumentId },
|
|
526
|
+
success: DocumentSource,
|
|
527
|
+
error: [
|
|
528
|
+
NotFound,
|
|
529
|
+
Forbidden,
|
|
530
|
+
InternalError
|
|
531
|
+
]
|
|
532
|
+
}), HttpApiEndpoint.post("create", "/", {
|
|
533
|
+
payload: CreateDocumentInput,
|
|
534
|
+
success: DocumentMutationResult.pipe(HttpApiSchema.status(201)),
|
|
535
|
+
error: MutationErrors
|
|
536
|
+
}), HttpApiEndpoint.put("update", "/:documentId", {
|
|
537
|
+
params: { documentId: DocumentId },
|
|
538
|
+
payload: UpdateDocumentPayload,
|
|
539
|
+
success: DocumentMutationResult,
|
|
540
|
+
error: MutationErrors
|
|
541
|
+
}), HttpApiEndpoint.post("validate", "/:documentId/validate", {
|
|
542
|
+
params: { documentId: DocumentId },
|
|
543
|
+
payload: ValidateDocumentPayload,
|
|
544
|
+
success: DocumentValidationResult,
|
|
545
|
+
error: [
|
|
546
|
+
NotFound,
|
|
547
|
+
VersionConflict,
|
|
548
|
+
Forbidden,
|
|
549
|
+
InternalError
|
|
550
|
+
]
|
|
551
|
+
}), HttpApiEndpoint.post("move", "/:documentId/move", {
|
|
552
|
+
params: { documentId: DocumentId },
|
|
553
|
+
payload: MoveDocumentPayload,
|
|
554
|
+
success: DocumentMutationResult,
|
|
555
|
+
error: MutationErrors
|
|
556
|
+
}), HttpApiEndpoint.post("duplicate", "/:documentId/duplicate", {
|
|
557
|
+
params: { documentId: DocumentId },
|
|
558
|
+
payload: Schema.Struct({
|
|
559
|
+
...MoveDocumentPayload.fields,
|
|
560
|
+
includeChildren: DuplicateDocumentInput.fields.includeChildren
|
|
561
|
+
}),
|
|
562
|
+
success: DocumentMutationResult,
|
|
563
|
+
error: MutationErrors
|
|
564
|
+
}), HttpApiEndpoint.post("unpublish", "/:documentId/unpublish", {
|
|
565
|
+
params: { documentId: DocumentId },
|
|
566
|
+
payload: PublishDocumentPayload,
|
|
567
|
+
success: DocumentMutationResult,
|
|
568
|
+
error: MutationErrors
|
|
569
|
+
}), HttpApiEndpoint.post("publish", "/:documentId/publish", {
|
|
570
|
+
params: { documentId: DocumentId },
|
|
571
|
+
payload: PublishDocumentPayload,
|
|
572
|
+
success: DocumentMutationResult,
|
|
573
|
+
error: MutationErrors
|
|
574
|
+
}), HttpApiEndpoint.post("remove", "/:documentId/remove", {
|
|
575
|
+
params: { documentId: DocumentId },
|
|
576
|
+
payload: RemoveDocumentPayload,
|
|
577
|
+
success: DocumentMutationResult,
|
|
578
|
+
error: MutationErrors
|
|
579
|
+
}), HttpApiEndpoint.post("restore", "/:documentId/restore", {
|
|
580
|
+
params: { documentId: DocumentId },
|
|
581
|
+
payload: RestoreDocumentPayload,
|
|
582
|
+
success: DocumentMutationResult,
|
|
583
|
+
error: MutationErrors
|
|
584
|
+
}), HttpApiEndpoint.post("undelete", "/:documentId/undelete", {
|
|
585
|
+
params: { documentId: DocumentId },
|
|
586
|
+
payload: UndeleteDocumentPayload,
|
|
587
|
+
success: DocumentMutationResult,
|
|
588
|
+
error: MutationErrors
|
|
589
|
+
})).middleware(StudioAuthentication).prefix("/documents");
|
|
590
|
+
const StudioMediaApi = HttpApiGroup.make("media").add(HttpApiEndpoint.get("search", "/", {
|
|
591
|
+
query: MediaQuery,
|
|
592
|
+
success: MediaPage,
|
|
593
|
+
error: [
|
|
594
|
+
MediaInvalidQuery,
|
|
595
|
+
Forbidden,
|
|
596
|
+
InternalError
|
|
597
|
+
]
|
|
598
|
+
}), HttpApiEndpoint.post("upload", "/", {
|
|
599
|
+
query: MediaUploadQuery,
|
|
600
|
+
payload: MediaUploadPayload,
|
|
601
|
+
success: MediaAsset.pipe(HttpApiSchema.status(201)),
|
|
602
|
+
error: [
|
|
603
|
+
MediaInvalidUpload,
|
|
604
|
+
MediaTooLargeResponse,
|
|
605
|
+
Forbidden,
|
|
606
|
+
InternalError
|
|
607
|
+
]
|
|
608
|
+
}), HttpApiEndpoint.put("replace", "/:mediaId", {
|
|
609
|
+
params: { mediaId: MediaId },
|
|
610
|
+
query: MediaUploadQuery,
|
|
611
|
+
payload: MediaUploadPayload,
|
|
612
|
+
success: MediaAsset,
|
|
613
|
+
error: [
|
|
614
|
+
MediaNotFoundResponse,
|
|
615
|
+
MediaConflictResponse,
|
|
616
|
+
MediaInvalidUpload,
|
|
617
|
+
MediaTooLargeResponse,
|
|
618
|
+
Forbidden,
|
|
619
|
+
InternalError
|
|
620
|
+
]
|
|
621
|
+
}), HttpApiEndpoint.post("cleanup", "/cleanup", {
|
|
622
|
+
success: MediaCleanupReport,
|
|
623
|
+
error: [Forbidden, InternalError]
|
|
624
|
+
}), HttpApiEndpoint.get("integrity", "/integrity", {
|
|
625
|
+
success: MediaIntegrityReport,
|
|
626
|
+
error: [Forbidden, InternalError]
|
|
627
|
+
}), HttpApiEndpoint.get("get", "/:mediaId", {
|
|
628
|
+
params: { mediaId: MediaId },
|
|
629
|
+
success: MediaAsset,
|
|
630
|
+
error: [
|
|
631
|
+
MediaNotFoundResponse,
|
|
632
|
+
Forbidden,
|
|
633
|
+
InternalError
|
|
634
|
+
]
|
|
635
|
+
}), HttpApiEndpoint.delete("remove", "/:mediaId", {
|
|
636
|
+
params: { mediaId: MediaId },
|
|
637
|
+
success: HttpApiSchema.NoContent,
|
|
638
|
+
error: [
|
|
639
|
+
MediaNotFoundResponse,
|
|
640
|
+
MediaConflictResponse,
|
|
641
|
+
MediaReferencedResponse,
|
|
642
|
+
Forbidden,
|
|
643
|
+
InternalError
|
|
644
|
+
]
|
|
645
|
+
})).middleware(StudioAuthentication).prefix("/media");
|
|
646
|
+
const StudioConfigurationApi = HttpApiGroup.make("configuration").add(HttpApiEndpoint.get("bootstrap", "/bootstrap", { success: StudioBootstrap })).middleware(StudioAuthentication);
|
|
647
|
+
const StudioSessionApi = HttpApiGroup.make("session").add(HttpApiEndpoint.get("current", "/session", { success: StudioActor })).middleware(StudioAuthentication);
|
|
648
|
+
const InitializationApi = HttpApiGroup.make("initialization").add(HttpApiEndpoint.get("status", "/initialization", {
|
|
649
|
+
success: InitializationState,
|
|
650
|
+
error: InternalError
|
|
651
|
+
}), HttpApiEndpoint.post("claim", "/initialization", {
|
|
652
|
+
payload: InitializationInput,
|
|
653
|
+
success: User.pipe(HttpApiSchema.status(201)),
|
|
654
|
+
error: [
|
|
655
|
+
AlreadyInitialized,
|
|
656
|
+
InitializationProofRejected,
|
|
657
|
+
InternalError
|
|
658
|
+
]
|
|
659
|
+
}));
|
|
660
|
+
const StudioDiagnosticsApi = HttpApiGroup.make("diagnostics").add(HttpApiEndpoint.get("documents", "/diagnostics/documents", {
|
|
661
|
+
success: Schema.Array(DocumentIssue),
|
|
662
|
+
error: [Forbidden, InternalError]
|
|
663
|
+
})).middleware(StudioAuthentication);
|
|
664
|
+
const StudioHistoryApi = HttpApiGroup.make("history").add(HttpApiEndpoint.get("list", "/", {
|
|
665
|
+
query: DocumentHistoryQuery,
|
|
666
|
+
success: DocumentRevisionPage,
|
|
667
|
+
error: [
|
|
668
|
+
InvalidHistoryCursor,
|
|
669
|
+
Forbidden,
|
|
670
|
+
InternalError
|
|
671
|
+
]
|
|
672
|
+
}), HttpApiEndpoint.get("read", "/:documentId/:revisionId", {
|
|
673
|
+
params: {
|
|
674
|
+
documentId: DocumentId,
|
|
675
|
+
revisionId: DocumentRevision.fields.revisionId
|
|
676
|
+
},
|
|
677
|
+
success: DocumentRevision,
|
|
678
|
+
error: [
|
|
679
|
+
NotFound,
|
|
680
|
+
Forbidden,
|
|
681
|
+
InternalError
|
|
682
|
+
]
|
|
683
|
+
}), HttpApiEndpoint.post("compare", "/compare", {
|
|
684
|
+
payload: CompareDocumentRevisionsInput,
|
|
685
|
+
success: DocumentRevisionComparison,
|
|
686
|
+
error: [
|
|
687
|
+
NotFound,
|
|
688
|
+
HistoryComparisonTooLarge,
|
|
689
|
+
Forbidden,
|
|
690
|
+
InternalError
|
|
691
|
+
]
|
|
692
|
+
})).middleware(StudioAuthentication).prefix("/history");
|
|
693
|
+
const StudioActivityApi = HttpApiGroup.make("activity").add(HttpApiEndpoint.get("list", "/", {
|
|
694
|
+
query: ActivityQuery,
|
|
695
|
+
success: ActivityPage,
|
|
696
|
+
error: [
|
|
697
|
+
InvalidActivityCursor,
|
|
698
|
+
Forbidden,
|
|
699
|
+
InternalError
|
|
700
|
+
]
|
|
701
|
+
})).middleware(StudioAuthentication).prefix("/activity");
|
|
702
|
+
const StudioSeoApi = HttpApiGroup.make("seo").add(HttpApiEndpoint.get("read", "/", {
|
|
703
|
+
success: SeoState,
|
|
704
|
+
error: [Forbidden, InternalError]
|
|
705
|
+
}), HttpApiEndpoint.put("save", "/", {
|
|
706
|
+
payload: SaveSeoInput,
|
|
707
|
+
success: SeoMutationResult,
|
|
708
|
+
error: [
|
|
709
|
+
SeoConflict,
|
|
710
|
+
SeoConfirmation,
|
|
711
|
+
SeoMediaInvalid,
|
|
712
|
+
Forbidden,
|
|
713
|
+
InternalError
|
|
714
|
+
]
|
|
715
|
+
}), HttpApiEndpoint.post("publish", "/publish", {
|
|
716
|
+
payload: PublishSeoInput,
|
|
717
|
+
success: SeoMutationResult,
|
|
718
|
+
error: [
|
|
719
|
+
SeoConflict,
|
|
720
|
+
SeoConfirmation,
|
|
721
|
+
SeoMediaInvalid,
|
|
722
|
+
Forbidden,
|
|
723
|
+
InternalError
|
|
724
|
+
]
|
|
725
|
+
})).middleware(StudioAuthentication).prefix("/seo");
|
|
726
|
+
const StudioUserAdministrationApi = HttpApiGroup.make("users").add(HttpApiEndpoint.get("list", "/", {
|
|
727
|
+
query: UserQuery,
|
|
728
|
+
success: UserPage,
|
|
729
|
+
error: [UserAdministrationForbidden, InternalError]
|
|
730
|
+
}), HttpApiEndpoint.post("create", "/", {
|
|
731
|
+
payload: CreateUserInput,
|
|
732
|
+
success: User.pipe(HttpApiSchema.status(201)),
|
|
733
|
+
error: [
|
|
734
|
+
UserDuplicate,
|
|
735
|
+
UserAdministrationForbidden,
|
|
736
|
+
InternalError
|
|
737
|
+
]
|
|
738
|
+
}), HttpApiEndpoint.post("changeRole", "/:userId/role", {
|
|
739
|
+
params: { userId: User.fields.id },
|
|
740
|
+
payload: ChangeRolePayload,
|
|
741
|
+
success: User,
|
|
742
|
+
error: [
|
|
743
|
+
MissingUser,
|
|
744
|
+
UserSelfRoleChange,
|
|
745
|
+
UserAdministrationForbidden,
|
|
746
|
+
InternalError
|
|
747
|
+
]
|
|
748
|
+
}), HttpApiEndpoint.post("revokeSessions", "/:userId/sessions/revoke", {
|
|
749
|
+
params: { userId: User.fields.id },
|
|
750
|
+
success: User,
|
|
751
|
+
error: [
|
|
752
|
+
MissingUser,
|
|
753
|
+
UserAdministrationForbidden,
|
|
754
|
+
InternalError
|
|
755
|
+
]
|
|
756
|
+
}), HttpApiEndpoint.delete("remove", "/:userId", {
|
|
757
|
+
params: { userId: User.fields.id },
|
|
758
|
+
success: User,
|
|
759
|
+
error: [
|
|
760
|
+
MissingUser,
|
|
761
|
+
UserSelfRemoval,
|
|
762
|
+
UserAdministrationForbidden,
|
|
763
|
+
InternalError
|
|
764
|
+
]
|
|
765
|
+
})).middleware(StudioAuthentication).prefix("/users");
|
|
766
|
+
const StudioCollaborationApi = HttpApiGroup.make("collaboration").add(HttpApiEndpoint.post("ticket", "/:documentId/ticket", {
|
|
767
|
+
params: { documentId: DocumentId },
|
|
768
|
+
success: CollaborationTicket,
|
|
769
|
+
error: [
|
|
770
|
+
NotFound,
|
|
771
|
+
Forbidden,
|
|
772
|
+
InternalError
|
|
773
|
+
]
|
|
774
|
+
})).middleware(StudioAuthentication).prefix("/collaboration");
|
|
775
|
+
const FormsApi = HttpApiGroup.make("forms").add(HttpApiEndpoint.post("submit", "/submit", {
|
|
776
|
+
payload: FormSubmission,
|
|
777
|
+
success: FormResult
|
|
778
|
+
})).prefix("/forms");
|
|
779
|
+
var StudioApi = class extends HttpApi.make("StudioApi").add(FormsApi, InitializationApi, PublishedDeliveryApi, StudioConfigurationApi, StudioSessionApi, StudioDiagnosticsApi, PreviewDeliveryApi.middleware(StudioAuthentication), StudioDocumentsApi, StudioHistoryApi, StudioActivityApi, StudioSeoApi, StudioUserAdministrationApi, StudioMediaApi, StudioCollaborationApi).middleware(StudioSchemaError).prefix(STUDIO_API_PATH) {};
|
|
780
|
+
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region ../document/src/Seo.ts
|
|
783
|
+
function resolveDocumentSeo({ document, global, capabilities }) {
|
|
784
|
+
const authored = document.seo;
|
|
785
|
+
const baseTitle = authored?.title ?? document.title;
|
|
786
|
+
const title = global.titleTemplate?.replaceAll("%s", baseTitle) ?? baseTitle;
|
|
787
|
+
const description = authored?.description ?? document.description ?? global.defaultDescription;
|
|
788
|
+
const socialTitle = authored?.socialTitle ?? global.socialTitle ?? title;
|
|
789
|
+
const socialDescription = authored?.socialDescription ?? global.socialDescription ?? description;
|
|
790
|
+
const socialImage = authored?.socialImage ?? global.socialImage;
|
|
791
|
+
const canonicalUrl = authored?.canonicalUrl ?? SeoUrl.make(new URL(document.path, `${global.canonicalOrigin}/`).href);
|
|
792
|
+
const structuredData = resolveStructuredData(authored?.structuredData, capabilities);
|
|
793
|
+
const identity = resolveIdentity(global.identity, capabilities);
|
|
794
|
+
return {
|
|
795
|
+
title,
|
|
796
|
+
...description === void 0 ? void 0 : { description },
|
|
797
|
+
socialTitle,
|
|
798
|
+
...socialDescription === void 0 ? void 0 : { socialDescription },
|
|
799
|
+
...socialImage === void 0 ? void 0 : { socialImage },
|
|
800
|
+
canonicalUrl,
|
|
801
|
+
index: global.index && (authored?.index ?? true),
|
|
802
|
+
follow: global.follow && (authored?.follow ?? true),
|
|
803
|
+
...structuredData === void 0 ? void 0 : { structuredData },
|
|
804
|
+
...identity === void 0 ? void 0 : { identity }
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function resolveStructuredData(authored, capabilities) {
|
|
808
|
+
if (authored && capabilities.structuredDataTypes.includes(authored.type)) return authored;
|
|
809
|
+
return capabilities.structuredDataTypes.includes("WebPage") ? { type: "WebPage" } : void 0;
|
|
810
|
+
}
|
|
811
|
+
function resolveIdentity(identity, capabilities) {
|
|
812
|
+
return identity && capabilities.identityTypes.includes(identity.type) ? identity : void 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region ../server/src/internal/access-control.ts
|
|
817
|
+
const VIEWER_ACTIONS = /* @__PURE__ */ new Set([
|
|
818
|
+
"document.browse",
|
|
819
|
+
"document.read",
|
|
820
|
+
"document.trash",
|
|
821
|
+
"document.history",
|
|
822
|
+
"media.read",
|
|
823
|
+
"media.search",
|
|
824
|
+
"media.integrity"
|
|
825
|
+
]);
|
|
826
|
+
const EDITOR_ACTIONS = /* @__PURE__ */ new Set([...DOCUMENT_ACTIONS, ...MEDIA_ACTIONS.filter((action) => action !== "media.cleanup")]);
|
|
827
|
+
var AccessControl = class extends Context.Service()("@internal/server/AccessControl") {};
|
|
828
|
+
const defaultAccessControlLayer = Layer.succeed(AccessControl, AccessControl.of({ require: Effect.fn("AccessControl.require")(function* (context, decision) {
|
|
829
|
+
if (isAllowed(context, decision.action)) return;
|
|
830
|
+
return yield* new AccessDenied({
|
|
831
|
+
action: decision.action,
|
|
832
|
+
actorId: context.actor.id,
|
|
833
|
+
resource: decision.resource
|
|
834
|
+
});
|
|
835
|
+
}) }));
|
|
836
|
+
function isAllowed(context, action) {
|
|
837
|
+
return context.actor.roles.some((role) => {
|
|
838
|
+
if (role === "administrator") return true;
|
|
839
|
+
return role === "editor" && EDITOR_ACTIONS.has(action) || role === "viewer" && VIEWER_ACTIONS.has(action);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region ../document/src/internal/editor/render.ts
|
|
845
|
+
const renderEditorMarkdown = Effect.fn("EditorDocument.renderMarkdown")((document) => {
|
|
846
|
+
return Effect.tryPromise({
|
|
847
|
+
try: () => renderMarkdown(toComarkDocument(withoutEditorMetadata(document))),
|
|
848
|
+
catch: (cause) => new EditorRenderFailed({ cause })
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region ../document/src/internal/document-engine/relocate-links.ts
|
|
854
|
+
/** Rewrites only semantic page links, preserving bindings and unrelated content. */
|
|
855
|
+
function rewriteDocumentRoutes(document, destinations, components = []) {
|
|
856
|
+
let changed = false;
|
|
857
|
+
function link(value) {
|
|
858
|
+
const target = referenceTarget("a", /* @__PURE__ */ new Map([["href", value]]), document.path, void 0);
|
|
859
|
+
if (target?.kind !== "document" || !Predicate.isString(value)) return value;
|
|
860
|
+
const destination = destinations.get(target.path);
|
|
861
|
+
const sourceMoved = destinations.has(document.path);
|
|
862
|
+
if (!destination && (!sourceMoved || value.startsWith("/"))) return value;
|
|
863
|
+
const suffix = value.match(/[?#].*$/u)?.[0] ?? "";
|
|
864
|
+
const next = `${destination ?? target.path}${suffix}`;
|
|
865
|
+
if (next !== value) changed = true;
|
|
866
|
+
return next;
|
|
867
|
+
}
|
|
868
|
+
function properties(fields, values) {
|
|
869
|
+
const result = mutableJsonObject(values);
|
|
870
|
+
for (const field of fields) {
|
|
871
|
+
const value = values[field.name] === void 0 ? field.defaultValue : values[field.name];
|
|
872
|
+
if (field.input === "link") {
|
|
873
|
+
const next = link(value);
|
|
874
|
+
if (next !== void 0 && next !== value) result[field.name] = next;
|
|
875
|
+
} else if (field.fields) {
|
|
876
|
+
const next = Array.isArray(value) ? value.map((item) => Schema.is(Schema.JsonObject)(item) ? properties(field.fields ?? [], item) : item) : Schema.is(Schema.JsonObject)(value) ? properties(field.fields, value) : value;
|
|
877
|
+
if (next !== void 0 && JSON.stringify(next) !== JSON.stringify(value)) result[field.name] = next;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
function visit(node, renderData) {
|
|
883
|
+
if (Predicate.isString(node) || node[0] === null) return node;
|
|
884
|
+
const [tag, attributes, ...children] = node;
|
|
885
|
+
const projection = projectCmsEditorComponentAttributes(attributes, renderData);
|
|
886
|
+
let props = properties(components.find((component) => component.name.toLowerCase() === tag.toLowerCase())?.authoring.props ?? [], projection.props);
|
|
887
|
+
if (tag === "a") {
|
|
888
|
+
const href = link(projection.props.href);
|
|
889
|
+
if (href !== void 0) props = Object.assign({}, props, { href });
|
|
890
|
+
}
|
|
891
|
+
return [
|
|
892
|
+
tag,
|
|
893
|
+
JSON.stringify(props) === JSON.stringify(projection.props) ? attributes : encodeCmsEditorComponentAttributes(props, projection.bindings),
|
|
894
|
+
...children.map((child) => visit(child, projection.renderData))
|
|
895
|
+
];
|
|
896
|
+
}
|
|
897
|
+
const nodes = document.body.nodes.map((node) => visit(node, createCmsComarkRenderData(document.body)));
|
|
898
|
+
return changed ? Object.assign({}, document.body, { nodes }) : void 0;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region ../document/src/DocumentPlanning.ts
|
|
903
|
+
function planDocumentCreation(configuration, input) {
|
|
904
|
+
return Result.gen(function* () {
|
|
905
|
+
const collection = configuration.types.find((candidate) => candidate.collection === input.collection);
|
|
906
|
+
if (!collection) return yield* invalid$1(`Document collection "${input.collection}" does not exist.`, "collection");
|
|
907
|
+
const title = input.title.trim();
|
|
908
|
+
if (!title) return yield* invalid$1("A document title is required.", "title");
|
|
909
|
+
const locale = yield* resolveLocale(configuration, input.locale);
|
|
910
|
+
const address = yield* parseDocumentAddress(collection.addressPattern).pipe(Result.mapError(addressInvalid));
|
|
911
|
+
const relativePath = yield* resolveRelativePath(collection, input.location);
|
|
912
|
+
const data = Object.fromEntries(address.tokens.flatMap((token) => {
|
|
913
|
+
if (token.name === "slug" && relativePath) return [[token.name, relativePath.split("/").at(-1) ?? relativePath]];
|
|
914
|
+
return [];
|
|
915
|
+
}));
|
|
916
|
+
const relativeKey = yield* normalizePublicPath(relativePath).pipe(Result.mapError(addressInvalid));
|
|
917
|
+
const key = yield* resolveDocumentAddress(address, relativeKey, data).pipe(Result.mapError(addressInvalid));
|
|
918
|
+
const sourcePath = documentSourcePath(collection, relativePath, locale);
|
|
919
|
+
const path = localizeDocumentPath(key, locale, configuration.locales);
|
|
920
|
+
return Object.assign({
|
|
921
|
+
collection,
|
|
922
|
+
data,
|
|
923
|
+
key,
|
|
924
|
+
path,
|
|
925
|
+
sourcePath,
|
|
926
|
+
title
|
|
927
|
+
}, locale === void 0 ? void 0 : { locale });
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
const createInitialDocumentSource = Effect.fn("DocumentPlanning.createInitialSource")(function* (plan) {
|
|
931
|
+
const body = CmsMarkdownDocument.make({
|
|
932
|
+
frontmatter: initialFrontmatter(plan),
|
|
933
|
+
meta: {},
|
|
934
|
+
nodes: [[
|
|
935
|
+
"h1",
|
|
936
|
+
{},
|
|
937
|
+
plan.title
|
|
938
|
+
]]
|
|
939
|
+
});
|
|
940
|
+
return {
|
|
941
|
+
body,
|
|
942
|
+
markdown: yield* renderEditorMarkdown(body)
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
function planDocumentMove(configuration, document, location) {
|
|
946
|
+
return Result.gen(function* () {
|
|
947
|
+
const collection = configuration.types.find((candidate) => candidate.collection === document.collection);
|
|
948
|
+
if (!collection) return yield* invalid$1(`Document collection "${document.collection}" does not exist.`, "documentId");
|
|
949
|
+
return documentSourcePath(collection, yield* resolveRelativePath(collection, location), document.locale);
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
function documentRelativePath(collection, entry) {
|
|
953
|
+
let sourcePath = entry.sourcePath;
|
|
954
|
+
if (entry.document.locale && sourcePath.startsWith(`${entry.document.locale}/`)) sourcePath = sourcePath.slice(entry.document.locale.length + 1);
|
|
955
|
+
if (collection.directory !== "." && sourcePath.startsWith(`${collection.directory}/`)) sourcePath = sourcePath.slice(collection.directory.length + 1);
|
|
956
|
+
sourcePath = sourcePath.replace(/\.(?:md|mdc)$/iu, "");
|
|
957
|
+
return sourcePath.toLocaleLowerCase() === "index" ? void 0 : sourcePath.replace(/\/index$/iu, "");
|
|
958
|
+
}
|
|
959
|
+
function documentSourcePath(collection, relativePath, locale) {
|
|
960
|
+
const sourceName = relativePath ? `${relativePath}.md` : "index.md";
|
|
961
|
+
return SourcePath.make([
|
|
962
|
+
locale,
|
|
963
|
+
collection.directory === "." ? void 0 : collection.directory,
|
|
964
|
+
sourceName
|
|
965
|
+
].filter((value) => value !== void 0).join("/"));
|
|
966
|
+
}
|
|
967
|
+
function initialFrontmatter(plan) {
|
|
968
|
+
let frontmatter = {
|
|
969
|
+
...plan.data,
|
|
970
|
+
title: plan.title
|
|
971
|
+
};
|
|
972
|
+
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);
|
|
973
|
+
return frontmatter;
|
|
974
|
+
}
|
|
975
|
+
function nestedValue$1(value, path) {
|
|
976
|
+
return path.split(".").reduce((current, segment) => isJsonObject(current) ? current[segment] : void 0, value);
|
|
977
|
+
}
|
|
978
|
+
function setNestedValue(target, path, value) {
|
|
979
|
+
const [segment, ...rest] = path;
|
|
980
|
+
if (!segment) return target;
|
|
981
|
+
if (rest.length === 0) return Object.assign({}, target, { [segment]: value });
|
|
982
|
+
const current = target[segment];
|
|
983
|
+
return Object.assign({}, target, { [segment]: setNestedValue(isJsonObject(current) ? current : {}, rest, value) });
|
|
984
|
+
}
|
|
985
|
+
const isJsonObject = Schema.is(Schema.JsonObject);
|
|
986
|
+
function resolveLocale(configuration, requested) {
|
|
987
|
+
const locales = configuration.locales;
|
|
988
|
+
if (!locales) return requested === void 0 ? Result.succeed(void 0) : invalid$1(`Document locale "${requested}" is not configured.`, "locale");
|
|
989
|
+
const locale = requested ?? locales.defaultLocale;
|
|
990
|
+
return locales.locales.includes(locale) ? Result.succeed(locale) : invalid$1(`Document locale "${locale}" does not exist.`, "locale");
|
|
991
|
+
}
|
|
992
|
+
function resolveRelativePath(collection, location) {
|
|
993
|
+
if (location.kind === "home") return collection.addressPattern === "/:path*" ? Result.succeed("") : invalid$1("Only a root catch-all collection can use the home location.", "location");
|
|
994
|
+
if (location.parent !== void 0 && !collection.hierarchy) return invalid$1(`Document collection "${collection.collection}" does not allow hierarchy.`, "location.parent");
|
|
995
|
+
return normalizeDocumentRelativePath([location.parent, location.slug].filter((value) => value !== void 0).join("/")).pipe(Result.mapError((error) => addressInvalid({
|
|
996
|
+
...error,
|
|
997
|
+
field: "location"
|
|
998
|
+
})));
|
|
999
|
+
}
|
|
1000
|
+
function addressInvalid(error) {
|
|
1001
|
+
return new DocumentValidationFailed({ issues: [validationIssue(error.message, error.field)] });
|
|
1002
|
+
}
|
|
1003
|
+
function invalid$1(message, field) {
|
|
1004
|
+
return Result.fail(new DocumentValidationFailed({ issues: [validationIssue(message, field)] }));
|
|
1005
|
+
}
|
|
1006
|
+
function validationIssue(message, field) {
|
|
1007
|
+
return field === void 0 ? {
|
|
1008
|
+
code: "document.invalid",
|
|
1009
|
+
message,
|
|
1010
|
+
severity: "error"
|
|
1011
|
+
} : {
|
|
1012
|
+
code: "document.invalid",
|
|
1013
|
+
field,
|
|
1014
|
+
message,
|
|
1015
|
+
severity: "error"
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
/** Updates route-owned frontmatter when relocating a page in a slug-addressed collection. */
|
|
1019
|
+
const relocateDocumentMarkdown = Effect.fn("DocumentPlanning.relocateMarkdown")(function* (configuration, document, location, markdown) {
|
|
1020
|
+
const collection = configuration.types.find((type) => type.collection === document.collection);
|
|
1021
|
+
if (!collection || !collection.addressPattern.includes(":slug") || location.kind === "home") return markdown;
|
|
1022
|
+
return yield* renderEditorMarkdown({
|
|
1023
|
+
...document.body,
|
|
1024
|
+
frontmatter: {
|
|
1025
|
+
...document.body.frontmatter,
|
|
1026
|
+
slug: location.slug
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region ../server/src/internal/documents/document-state.ts
|
|
1033
|
+
const isDocumentCompilationFailed = Schema.is(DocumentCompilationFailed);
|
|
1034
|
+
function makeDocumentState(authored, published, snapshot) {
|
|
1035
|
+
const activeById = /* @__PURE__ */ new Map();
|
|
1036
|
+
for (const source of snapshot.active) {
|
|
1037
|
+
if (activeById.has(source.documentId)) throw new Error(`Document "${source.documentId}" appears more than once in the repository snapshot.`);
|
|
1038
|
+
activeById.set(source.documentId, source);
|
|
1039
|
+
}
|
|
1040
|
+
const authoredById = /* @__PURE__ */ new Map();
|
|
1041
|
+
for (const document of authored.documents) {
|
|
1042
|
+
if (authoredById.has(document.id)) throw new Error(`Compiled document "${document.id}" appears more than once in the authored catalog.`);
|
|
1043
|
+
if (!activeById.has(document.id)) throw new Error(`Compiled document "${document.id}" is missing from the repository snapshot.`);
|
|
1044
|
+
authoredById.set(document.id, document);
|
|
1045
|
+
}
|
|
1046
|
+
for (const documentId of activeById.keys()) if (!authoredById.has(documentId)) throw new Error(`Compiled document "${documentId}" is missing from the repository snapshot.`);
|
|
1047
|
+
return {
|
|
1048
|
+
activeById,
|
|
1049
|
+
authored,
|
|
1050
|
+
authoredById,
|
|
1051
|
+
published,
|
|
1052
|
+
snapshot
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
const compileDocumentState = Effect.fn("DocumentState.compile")(function* (engine, snapshot, previous) {
|
|
1056
|
+
return makeDocumentState(yield* engine.compileAll({
|
|
1057
|
+
previous: previous?.authored,
|
|
1058
|
+
sources: authoredSources(snapshot)
|
|
1059
|
+
}), yield* engine.compileAll({
|
|
1060
|
+
previous: previous?.published,
|
|
1061
|
+
sources: publishedSources(snapshot)
|
|
1062
|
+
}), snapshot);
|
|
1063
|
+
});
|
|
1064
|
+
function authoredSources(snapshot) {
|
|
1065
|
+
return snapshot.active.map((source) => ({
|
|
1066
|
+
documentId: source.documentId,
|
|
1067
|
+
draft: source.publishedVersion !== source.version,
|
|
1068
|
+
markdown: source.markdown,
|
|
1069
|
+
sourcePath: source.sourcePath
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
function publishedSources(snapshot) {
|
|
1073
|
+
return snapshot.published.map((source) => ({
|
|
1074
|
+
documentId: source.documentId,
|
|
1075
|
+
draft: false,
|
|
1076
|
+
markdown: source.markdown,
|
|
1077
|
+
publishedAt: source.editedAt,
|
|
1078
|
+
sourcePath: source.sourcePath
|
|
1079
|
+
}));
|
|
1080
|
+
}
|
|
1081
|
+
const compileValidCatalog = Effect.fn("DocumentState.compileValid")(function* (engine, sources, previous) {
|
|
1082
|
+
const result = yield* Effect.result(engine.compileAll({
|
|
1083
|
+
previous,
|
|
1084
|
+
sources
|
|
1085
|
+
}));
|
|
1086
|
+
if (Result.isFailure(result)) return yield* new DocumentValidationFailed({ issues: compilationIssues(result.failure) });
|
|
1087
|
+
const errors = result.success.issues.filter((issue) => issue.severity === "error");
|
|
1088
|
+
if (errors.length > 0) return yield* new DocumentValidationFailed({ issues: errors });
|
|
1089
|
+
return result.success;
|
|
1090
|
+
});
|
|
1091
|
+
const validateDocumentSources = Effect.fn("DocumentState.validate")(function* (engine, sources, sourcePath, previous) {
|
|
1092
|
+
const result = yield* Effect.result(engine.compileAll({
|
|
1093
|
+
previous,
|
|
1094
|
+
sources
|
|
1095
|
+
}));
|
|
1096
|
+
if (Result.isFailure(result)) return DocumentValidationResult.make({
|
|
1097
|
+
issues: compilationIssues(result.failure),
|
|
1098
|
+
valid: false
|
|
1099
|
+
});
|
|
1100
|
+
const catalog = result.success;
|
|
1101
|
+
const document = catalog.documents.find((candidate) => candidate.source.path === sourcePath);
|
|
1102
|
+
const issues = catalog.issues;
|
|
1103
|
+
const validation = {
|
|
1104
|
+
issues,
|
|
1105
|
+
valid: document !== void 0 && !issues.some((issue) => issue.severity === "error")
|
|
1106
|
+
};
|
|
1107
|
+
return DocumentValidationResult.make(Object.assign(validation, document === void 0 ? void 0 : { document }));
|
|
1108
|
+
});
|
|
1109
|
+
function authoringEntries(state, query) {
|
|
1110
|
+
return state.snapshot.active.flatMap((source) => {
|
|
1111
|
+
const document = state.authoredById.get(source.documentId);
|
|
1112
|
+
if (!document) throw new Error(`Compiled document "${source.documentId}" is missing from the repository snapshot.`);
|
|
1113
|
+
if (query.collection !== void 0 && document.collection !== query.collection) return [];
|
|
1114
|
+
if (query.locale !== void 0 && document.locale !== query.locale) return [];
|
|
1115
|
+
return [DocumentSummary.make({
|
|
1116
|
+
...browseMetadata(state, source),
|
|
1117
|
+
document,
|
|
1118
|
+
sourcePath: source.sourcePath,
|
|
1119
|
+
version: source.version
|
|
1120
|
+
})];
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
function deletedEntries(state) {
|
|
1124
|
+
return state.snapshot.deleted.map((source) => DeletedDocument.make({
|
|
1125
|
+
...source.title ? { title: source.title } : void 0,
|
|
1126
|
+
...source.path ? { path: source.path } : void 0,
|
|
1127
|
+
...source.trashBatchId ? { trashBatchId: source.trashBatchId } : void 0,
|
|
1128
|
+
documentId: source.documentId,
|
|
1129
|
+
deletedAt: source.deletedAt,
|
|
1130
|
+
sourcePath: source.sourcePath,
|
|
1131
|
+
version: source.version
|
|
1132
|
+
}));
|
|
1133
|
+
}
|
|
1134
|
+
function entryById(state, documentId) {
|
|
1135
|
+
const source = state.activeById.get(documentId);
|
|
1136
|
+
const document = state.authoredById.get(documentId);
|
|
1137
|
+
return source && document ? DocumentSummary.make({
|
|
1138
|
+
...browseMetadata(state, source),
|
|
1139
|
+
document,
|
|
1140
|
+
sourcePath: source.sourcePath,
|
|
1141
|
+
version: source.version
|
|
1142
|
+
}) : void 0;
|
|
1143
|
+
}
|
|
1144
|
+
function compilationIssues(error) {
|
|
1145
|
+
if (isDocumentCompilationFailed(error)) return error.issues;
|
|
1146
|
+
return [{
|
|
1147
|
+
code: "markdown.parse",
|
|
1148
|
+
message: errorMessage(error.cause),
|
|
1149
|
+
severity: "error",
|
|
1150
|
+
sourcePath: error.sourcePath
|
|
1151
|
+
}];
|
|
1152
|
+
}
|
|
1153
|
+
function errorMessage(cause) {
|
|
1154
|
+
return cause instanceof Error && cause.message ? cause.message : "Markdown could not be parsed.";
|
|
1155
|
+
}
|
|
1156
|
+
function browseMetadata(state, source) {
|
|
1157
|
+
const live = state.published.documents.find((document) => document.id === source.documentId);
|
|
1158
|
+
const metadata = {};
|
|
1159
|
+
if (source.createdBy) metadata.createdBy = source.createdBy;
|
|
1160
|
+
if (source.editedBy) metadata.editedBy = source.editedBy;
|
|
1161
|
+
if (source.editedAt) metadata.editedAt = source.editedAt;
|
|
1162
|
+
if (source.publishedVersion) metadata.publishedVersion = source.publishedVersion;
|
|
1163
|
+
if (live) metadata.publishedPath = live.path;
|
|
1164
|
+
return metadata;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
//#endregion
|
|
1168
|
+
//#region ../server/src/internal/documents/document-tree-mutation.ts
|
|
1169
|
+
/** Selects by route boundaries, never by title or a raw prefix such as /about matching /about-us. */
|
|
1170
|
+
function subtreeIds(state, documentId) {
|
|
1171
|
+
const parent = state.authoredById.get(documentId);
|
|
1172
|
+
if (!parent) return [documentId];
|
|
1173
|
+
return state.authored.documents.filter((page) => page.id === parent.id || parent.key !== "/" && page.collection === parent.collection && page.locale === parent.locale && page.key.startsWith(`${parent.key}/`)).map((page) => page.id);
|
|
1174
|
+
}
|
|
1175
|
+
const prepareTreeMutation = Effect.fn("Documents.prepareTreeMutation")(function* (dependencies, state, command) {
|
|
1176
|
+
const { configuration, engine } = dependencies;
|
|
1177
|
+
const input = command.input;
|
|
1178
|
+
const root = command.action === "document.undelete" ? state.snapshot.deleted.find((source) => source.documentId === input.documentId) : state.activeById.get(input.documentId);
|
|
1179
|
+
if (!root) return yield* new DocumentNotFound({ documentId: input.documentId });
|
|
1180
|
+
if (root.version !== input.expectedVersion) return yield* new DocumentVersionConflict({
|
|
1181
|
+
documentId: root.documentId,
|
|
1182
|
+
actualVersion: root.version,
|
|
1183
|
+
expectedVersion: input.expectedVersion
|
|
1184
|
+
});
|
|
1185
|
+
let sources;
|
|
1186
|
+
if (command.action === "document.undelete") {
|
|
1187
|
+
const deleted = state.snapshot.deleted.find((source) => source.documentId === root.documentId);
|
|
1188
|
+
sources = state.snapshot.deleted.filter((source) => source.documentId === root.documentId || deleted && deleted.trashBatchId && source.trashBatchId === deleted.trashBatchId && deleted.path !== void 0 && source.path?.startsWith(`${deleted.path}/`));
|
|
1189
|
+
} else {
|
|
1190
|
+
const ids = new Set(command.action === "document.duplicate" && !command.input.includeChildren ? [root.documentId] : subtreeIds(state, root.documentId));
|
|
1191
|
+
sources = state.snapshot.active.filter((source) => ids.has(source.documentId));
|
|
1192
|
+
}
|
|
1193
|
+
sources = [root, ...sources.filter((source) => source.documentId !== root.documentId)];
|
|
1194
|
+
const plans = [];
|
|
1195
|
+
const parent = state.authoredById.get(root.documentId);
|
|
1196
|
+
for (const source of sources) {
|
|
1197
|
+
if (command.action === "document.remove" || command.action === "document.undelete") {
|
|
1198
|
+
const page = state.authoredById.get(source.documentId);
|
|
1199
|
+
plans.push({
|
|
1200
|
+
...command.action === "document.remove" ? {
|
|
1201
|
+
trashBatchId: root.version,
|
|
1202
|
+
title: page?.title,
|
|
1203
|
+
path: page?.path
|
|
1204
|
+
} : void 0,
|
|
1205
|
+
action: command.action,
|
|
1206
|
+
documentId: source.documentId,
|
|
1207
|
+
expectedVersion: source.version,
|
|
1208
|
+
markdown: source.markdown,
|
|
1209
|
+
sourcePath: source.sourcePath
|
|
1210
|
+
});
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const page = state.authoredById.get(source.documentId);
|
|
1214
|
+
const collection = configuration.types.find((type) => type.collection === page?.collection);
|
|
1215
|
+
if (!page || !parent || !collection) return yield* new DocumentNotFound({ documentId: source.documentId });
|
|
1216
|
+
if (command.action === "document.duplicate" && !collection.canCreate) return yield* invalid("This collection does not allow new pages.");
|
|
1217
|
+
const destination = command.input.location;
|
|
1218
|
+
if (parent.key === "/" && command.action === "document.move") return yield* invalid("The home page cannot be moved.");
|
|
1219
|
+
const rootRelative = documentRelativePath(collection, {
|
|
1220
|
+
document: parent,
|
|
1221
|
+
sourcePath: root.sourcePath
|
|
1222
|
+
}) ?? "";
|
|
1223
|
+
const nextRelative = destination.kind === "home" ? "" : [destination.parent, destination.slug].filter(Boolean).join("/");
|
|
1224
|
+
if (rootRelative && (nextRelative === rootRelative || nextRelative.startsWith(`${rootRelative}/`))) return yield* invalid("Choose a destination outside this page and its subpages.");
|
|
1225
|
+
const relative = documentRelativePath(collection, {
|
|
1226
|
+
document: page,
|
|
1227
|
+
sourcePath: source.sourcePath
|
|
1228
|
+
}) ?? "";
|
|
1229
|
+
const segments = `${nextRelative}${source.documentId === root.documentId ? "" : relative.slice(rootRelative.length)}`.split("/").filter(Boolean);
|
|
1230
|
+
const slug = segments.pop();
|
|
1231
|
+
const location = slug ? Object.assign({
|
|
1232
|
+
kind: "entry",
|
|
1233
|
+
slug: yield* decodeSlug(slug)
|
|
1234
|
+
}, segments.length ? { parent: yield* decodeParent(segments.join("/")) } : void 0) : { kind: "home" };
|
|
1235
|
+
const sourcePath = yield* Effect.fromResult(planDocumentMove(configuration, page, location));
|
|
1236
|
+
const markdown = yield* relocateDocumentMarkdown(configuration, page, location, source.markdown).pipe(Effect.mapError(() => invalid("The page could not be copied to its new URL.")));
|
|
1237
|
+
if (command.action === "document.duplicate") {
|
|
1238
|
+
const documentId = yield* command.nextDocumentId;
|
|
1239
|
+
plans.push({
|
|
1240
|
+
action: "document.create",
|
|
1241
|
+
documentId,
|
|
1242
|
+
markdown,
|
|
1243
|
+
sourcePath
|
|
1244
|
+
});
|
|
1245
|
+
} else plans.push({
|
|
1246
|
+
action: "document.move",
|
|
1247
|
+
documentId: source.documentId,
|
|
1248
|
+
expectedVersion: source.version,
|
|
1249
|
+
markdown,
|
|
1250
|
+
sourcePath,
|
|
1251
|
+
publishedVersion: source.publishedVersion
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
const changed = new Set(plans.map((plan) => plan.documentId));
|
|
1255
|
+
const nextSources = authoredSources(state.snapshot).filter((source) => !changed.has(source.documentId));
|
|
1256
|
+
if (command.action !== "document.remove") for (const plan of plans) {
|
|
1257
|
+
if (nextSources.some((source) => source.sourcePath === plan.sourcePath)) return yield* new DocumentPathConflict({ sourcePath: plan.sourcePath });
|
|
1258
|
+
nextSources.push({
|
|
1259
|
+
documentId: plan.documentId,
|
|
1260
|
+
draft: true,
|
|
1261
|
+
markdown: plan.markdown,
|
|
1262
|
+
sourcePath: plan.sourcePath
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
if (command.action === "document.move" || command.action === "document.duplicate") {
|
|
1266
|
+
const preliminary = yield* engine.compileAll({
|
|
1267
|
+
sources: nextSources,
|
|
1268
|
+
previous: state.authored
|
|
1269
|
+
});
|
|
1270
|
+
const destinations = /* @__PURE__ */ new Map();
|
|
1271
|
+
for (let index = 0; index < sources.length; index++) {
|
|
1272
|
+
const original = sources[index];
|
|
1273
|
+
const plan = plans[index];
|
|
1274
|
+
const oldPage = original && state.authoredById.get(original.documentId);
|
|
1275
|
+
const newPage = plan && preliminary.documents.find((document) => document.id === plan.documentId);
|
|
1276
|
+
if (oldPage && newPage) destinations.set(oldPage.path, newPage.path);
|
|
1277
|
+
}
|
|
1278
|
+
const originals = command.action === "document.move" ? state.snapshot.active : sources;
|
|
1279
|
+
for (const source of originals) {
|
|
1280
|
+
const page = state.authoredById.get(source.documentId);
|
|
1281
|
+
if (!page) continue;
|
|
1282
|
+
const body = rewriteDocumentRoutes(page, destinations, dependencies.components);
|
|
1283
|
+
if (!body) continue;
|
|
1284
|
+
const index = command.action === "document.duplicate" ? sources.findIndex((item) => item.documentId === source.documentId) : plans.findIndex((plan) => plan.documentId === source.documentId);
|
|
1285
|
+
const existing = index >= 0 ? plans[index] : void 0;
|
|
1286
|
+
const projected = existing && preliminary.documents.find((document) => document.id === existing.documentId);
|
|
1287
|
+
const markdown = yield* renderEditorMarkdown(Object.assign({}, body, { frontmatter: projected?.body.frontmatter ?? body.frontmatter })).pipe(Effect.mapError(() => invalid("Page links could not be updated.")));
|
|
1288
|
+
if (existing) plans[index] = {
|
|
1289
|
+
...existing,
|
|
1290
|
+
markdown
|
|
1291
|
+
};
|
|
1292
|
+
else plans.push({
|
|
1293
|
+
action: "document.update",
|
|
1294
|
+
documentId: source.documentId,
|
|
1295
|
+
expectedVersion: source.version,
|
|
1296
|
+
sourcePath: source.sourcePath,
|
|
1297
|
+
markdown,
|
|
1298
|
+
publishedVersion: source.publishedVersion
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
for (const plan of plans) {
|
|
1302
|
+
const index = nextSources.findIndex((source) => source.documentId === plan.documentId);
|
|
1303
|
+
const updated = {
|
|
1304
|
+
documentId: plan.documentId,
|
|
1305
|
+
sourcePath: plan.sourcePath,
|
|
1306
|
+
markdown: plan.markdown,
|
|
1307
|
+
draft: true
|
|
1308
|
+
};
|
|
1309
|
+
if (index >= 0) nextSources[index] = updated;
|
|
1310
|
+
else nextSources.push(updated);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const first = plans[0];
|
|
1314
|
+
if (!first) return yield* Effect.die(/* @__PURE__ */ new Error("Empty subtree mutation."));
|
|
1315
|
+
return {
|
|
1316
|
+
authored: yield* compileValidCatalog(engine, nextSources, state.authored),
|
|
1317
|
+
published: command.action === "document.remove" ? yield* compileValidCatalog(engine, publishedSources(state.snapshot).filter((source) => !changed.has(source.documentId)), state.published) : state.published,
|
|
1318
|
+
commitPlan: first,
|
|
1319
|
+
additionalPlans: plans.slice(1)
|
|
1320
|
+
};
|
|
1321
|
+
});
|
|
1322
|
+
const decodeSlug = (value) => Schema.decodeEffect(Slug)(value).pipe(Effect.mapError(() => invalid("Invalid URL ending.")));
|
|
1323
|
+
const decodeParent = (value) => Schema.decodeEffect(RelativeDocumentPath)(value).pipe(Effect.mapError(() => invalid("Invalid parent URL.")));
|
|
1324
|
+
function invalid(message) {
|
|
1325
|
+
return new DocumentValidationFailed({ issues: [{
|
|
1326
|
+
code: "document.invalid",
|
|
1327
|
+
severity: "error",
|
|
1328
|
+
message
|
|
1329
|
+
}] });
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region ../server/src/internal/documents/document-lifecycle.ts
|
|
1334
|
+
const PreparedDocumentMutation = Data.taggedEnum();
|
|
1335
|
+
const prepareDocumentMutation = Effect.fn("DocumentLifecycle.prepare")(function* (dependencies, state, command) {
|
|
1336
|
+
const { engine, repository } = dependencies;
|
|
1337
|
+
if (command.action === "document.move" || command.action === "document.remove" || command.action === "document.undelete" || command.action === "document.duplicate") return PreparedDocumentMutation.Commit(yield* prepareTreeMutation(dependencies, state, command));
|
|
1338
|
+
const authored = authoredSources(state.snapshot);
|
|
1339
|
+
const published = publishedSources(state.snapshot);
|
|
1340
|
+
switch (command.action) {
|
|
1341
|
+
case "document.create": {
|
|
1342
|
+
yield* ensurePathAvailable(state, command.sourcePath);
|
|
1343
|
+
const nextAuthored = [...authored, {
|
|
1344
|
+
documentId: command.documentId,
|
|
1345
|
+
draft: true,
|
|
1346
|
+
markdown: command.markdown,
|
|
1347
|
+
sourcePath: command.sourcePath
|
|
1348
|
+
}];
|
|
1349
|
+
return PreparedDocumentMutation.Commit({
|
|
1350
|
+
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1351
|
+
published: state.published,
|
|
1352
|
+
commitPlan: {
|
|
1353
|
+
action: command.action,
|
|
1354
|
+
documentId: command.documentId,
|
|
1355
|
+
markdown: command.markdown,
|
|
1356
|
+
sourcePath: command.sourcePath
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
case "document.update": {
|
|
1361
|
+
const current = yield* requireActive(state, command.input.documentId);
|
|
1362
|
+
yield* requireVersion(current, command.input.expectedVersion);
|
|
1363
|
+
const nextAuthored = replaceSource(authored, {
|
|
1364
|
+
documentId: current.documentId,
|
|
1365
|
+
draft: true,
|
|
1366
|
+
markdown: command.input.markdown,
|
|
1367
|
+
sourcePath: current.sourcePath
|
|
1368
|
+
});
|
|
1369
|
+
return PreparedDocumentMutation.Commit({
|
|
1370
|
+
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1371
|
+
published: state.published,
|
|
1372
|
+
commitPlan: {
|
|
1373
|
+
action: command.action,
|
|
1374
|
+
documentId: current.documentId,
|
|
1375
|
+
expectedVersion: command.input.expectedVersion,
|
|
1376
|
+
markdown: command.input.markdown,
|
|
1377
|
+
publishedVersion: current.publishedVersion,
|
|
1378
|
+
sourcePath: current.sourcePath
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
case "document.restore": {
|
|
1383
|
+
const current = yield* requireActive(state, command.input.documentId);
|
|
1384
|
+
yield* requireVersion(current, command.input.expectedVersion);
|
|
1385
|
+
const historical = yield* repository.revision(current.documentId, command.input.revisionId);
|
|
1386
|
+
const nextAuthored = replaceSource(authored, {
|
|
1387
|
+
documentId: current.documentId,
|
|
1388
|
+
draft: true,
|
|
1389
|
+
markdown: historical.markdown,
|
|
1390
|
+
sourcePath: current.sourcePath
|
|
1391
|
+
});
|
|
1392
|
+
return PreparedDocumentMutation.Commit({
|
|
1393
|
+
authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
|
|
1394
|
+
published: state.published,
|
|
1395
|
+
commitPlan: {
|
|
1396
|
+
action: command.action,
|
|
1397
|
+
documentId: current.documentId,
|
|
1398
|
+
expectedVersion: command.input.expectedVersion,
|
|
1399
|
+
markdown: historical.markdown,
|
|
1400
|
+
publishedVersion: current.publishedVersion,
|
|
1401
|
+
revisionId: command.input.revisionId,
|
|
1402
|
+
sourcePath: current.sourcePath
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
case "document.unpublish": {
|
|
1407
|
+
const current = yield* requireActive(state, command.input.documentId);
|
|
1408
|
+
yield* requireVersion(current, command.input.expectedVersion);
|
|
1409
|
+
return PreparedDocumentMutation.Commit({
|
|
1410
|
+
authored: yield* compileValidCatalog(engine, replaceSource(authored, {
|
|
1411
|
+
documentId: current.documentId,
|
|
1412
|
+
draft: true,
|
|
1413
|
+
markdown: current.markdown,
|
|
1414
|
+
sourcePath: current.sourcePath
|
|
1415
|
+
}), state.authored),
|
|
1416
|
+
published: yield* compileValidCatalog(engine, withoutDocument(published, current.documentId), state.published),
|
|
1417
|
+
commitPlan: {
|
|
1418
|
+
action: command.action,
|
|
1419
|
+
documentId: current.documentId,
|
|
1420
|
+
expectedVersion: current.version,
|
|
1421
|
+
markdown: current.markdown,
|
|
1422
|
+
sourcePath: current.sourcePath,
|
|
1423
|
+
revisionId: current.revisionId
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
case "document.publish": {
|
|
1428
|
+
const current = yield* requireActive(state, command.input.documentId);
|
|
1429
|
+
yield* requireVersion(current, command.input.expectedVersion);
|
|
1430
|
+
if (current.publishedVersion === current.version) {
|
|
1431
|
+
const entry = entryById(state, current.documentId);
|
|
1432
|
+
const result = {
|
|
1433
|
+
action: "document.publish",
|
|
1434
|
+
affectedDocumentIds: [current.documentId],
|
|
1435
|
+
documentId: current.documentId,
|
|
1436
|
+
generation: state.snapshot.generation,
|
|
1437
|
+
revisionId: current.revisionId,
|
|
1438
|
+
sourcePath: current.sourcePath,
|
|
1439
|
+
version: current.version
|
|
1440
|
+
};
|
|
1441
|
+
return PreparedDocumentMutation.Unchanged({ result: DocumentMutationResult.make(Object.assign(result, entry === void 0 ? void 0 : { entry })) });
|
|
1442
|
+
}
|
|
1443
|
+
const source = {
|
|
1444
|
+
documentId: current.documentId,
|
|
1445
|
+
draft: false,
|
|
1446
|
+
markdown: current.markdown,
|
|
1447
|
+
sourcePath: current.sourcePath
|
|
1448
|
+
};
|
|
1449
|
+
return PreparedDocumentMutation.Commit({
|
|
1450
|
+
authored: yield* compileValidCatalog(engine, replaceSource(authored, source), state.authored),
|
|
1451
|
+
published: yield* compileValidCatalog(engine, replaceSource(published, source), state.published),
|
|
1452
|
+
commitPlan: {
|
|
1453
|
+
action: command.action,
|
|
1454
|
+
documentId: current.documentId,
|
|
1455
|
+
expectedVersion: command.input.expectedVersion,
|
|
1456
|
+
markdown: current.markdown,
|
|
1457
|
+
revisionId: current.revisionId,
|
|
1458
|
+
sourcePath: current.sourcePath
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
const validateDocumentUpdate = Effect.fn("DocumentLifecycle.validateUpdate")(function* (engine, state, input) {
|
|
1465
|
+
const current = yield* requireActive(state, input.documentId);
|
|
1466
|
+
yield* requireVersion(current, input.expectedVersion);
|
|
1467
|
+
const source = {
|
|
1468
|
+
documentId: current.documentId,
|
|
1469
|
+
draft: true,
|
|
1470
|
+
markdown: input.markdown,
|
|
1471
|
+
sourcePath: current.sourcePath
|
|
1472
|
+
};
|
|
1473
|
+
return yield* validateDocumentSources(engine, replaceSource(authoredSources(state.snapshot), source), current.sourcePath, state.authored);
|
|
1474
|
+
});
|
|
1475
|
+
function requireActive(state, documentId) {
|
|
1476
|
+
const source = state.activeById.get(documentId);
|
|
1477
|
+
return source ? Effect.succeed(source) : Effect.fail(new DocumentNotFound({ documentId }));
|
|
1478
|
+
}
|
|
1479
|
+
function requireVersion(source, expectedVersion) {
|
|
1480
|
+
return source.version === expectedVersion ? Effect.void : Effect.fail(new DocumentVersionConflict({
|
|
1481
|
+
actualVersion: source.version,
|
|
1482
|
+
documentId: source.documentId,
|
|
1483
|
+
expectedVersion
|
|
1484
|
+
}));
|
|
1485
|
+
}
|
|
1486
|
+
function ensurePathAvailable(state, sourcePath, exceptDocumentId) {
|
|
1487
|
+
return state.snapshot.active.some((source) => source.sourcePath === sourcePath && source.documentId !== exceptDocumentId) ? Effect.fail(new DocumentPathConflict({ sourcePath })) : Effect.void;
|
|
1488
|
+
}
|
|
1489
|
+
function replaceSource(sources, source) {
|
|
1490
|
+
return [...sources.filter((candidate) => candidate.documentId !== source.documentId), source];
|
|
1491
|
+
}
|
|
1492
|
+
function withoutDocument(sources, documentId) {
|
|
1493
|
+
return sources.filter((source) => source.documentId !== documentId);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
//#endregion
|
|
1497
|
+
//#region ../server/src/internal/documents/document-media-references.ts
|
|
1498
|
+
function projectMediaReferences(authored, published, documentId) {
|
|
1499
|
+
const references = [...referencesFromCatalog(authored, documentId), ...referencesFromCatalog(published, documentId)];
|
|
1500
|
+
return [...new Map(references.map((reference) => [referenceIdentity(reference), reference])).values()];
|
|
1501
|
+
}
|
|
1502
|
+
function referencesFromCatalog(catalog, documentId) {
|
|
1503
|
+
return catalog.references.all.filter((reference) => reference.target.kind === "media" && (documentId === void 0 || reference.source.documentId === documentId)).map(toMediaReference);
|
|
1504
|
+
}
|
|
1505
|
+
function toMediaReference(reference) {
|
|
1506
|
+
if (reference.target.kind !== "media") throw new Error("Expected a media reference.");
|
|
1507
|
+
return MediaReference.make({
|
|
1508
|
+
documentId: reference.source.documentId,
|
|
1509
|
+
...reference.target.mediaId ? { mediaId: reference.target.mediaId } : void 0,
|
|
1510
|
+
nodePath: reference.nodePath,
|
|
1511
|
+
objectKey: MediaObjectKey.make(reference.target.key),
|
|
1512
|
+
path: reference.source.path,
|
|
1513
|
+
sourcePath: reference.source.sourcePath
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
function referenceIdentity(reference) {
|
|
1517
|
+
return `${reference.documentId}:${reference.sourcePath}:${reference.nodePath.join(".")}:${reference.objectKey}`;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
//#endregion
|
|
1521
|
+
//#region ../server/src/internal/documents/document-mutation.ts
|
|
1522
|
+
/** Authorize every affected document before the adapter commits the approved plan atomically. */
|
|
1523
|
+
const commitDocumentMutation = Effect.fn("Documents.commitMutation")(function* (accessControl, dependencies, current, command, activity) {
|
|
1524
|
+
if (command.action === "document.move" || command.action === "document.remove" || command.action === "document.duplicate") {
|
|
1525
|
+
const ids = command.action === "document.duplicate" && !command.input.includeChildren ? [command.input.documentId] : subtreeIds(current, command.input.documentId);
|
|
1526
|
+
for (const documentId of ids) yield* accessControl.require({
|
|
1527
|
+
actor: activity.actor,
|
|
1528
|
+
requestId: activity.requestId
|
|
1529
|
+
}, {
|
|
1530
|
+
action: command.action,
|
|
1531
|
+
resource: {
|
|
1532
|
+
kind: "document",
|
|
1533
|
+
documentId
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
if (command.action === "document.duplicate") {
|
|
1537
|
+
const source = current.authoredById.get(command.input.documentId);
|
|
1538
|
+
if (source) yield* accessControl.require({
|
|
1539
|
+
actor: activity.actor,
|
|
1540
|
+
requestId: activity.requestId
|
|
1541
|
+
}, {
|
|
1542
|
+
action: "document.create",
|
|
1543
|
+
resource: {
|
|
1544
|
+
kind: "collection",
|
|
1545
|
+
collection: source.collection,
|
|
1546
|
+
locale: source.locale
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const prepared = yield* prepareDocumentMutation(dependencies, current, command);
|
|
1552
|
+
if (command.action === "document.move" && Predicate.isTagged("Commit")(prepared)) {
|
|
1553
|
+
for (const plan of prepared.additionalPlans ?? []) if (plan.action === "document.update") yield* accessControl.require({
|
|
1554
|
+
actor: activity.actor,
|
|
1555
|
+
requestId: activity.requestId
|
|
1556
|
+
}, {
|
|
1557
|
+
action: "document.update",
|
|
1558
|
+
resource: {
|
|
1559
|
+
kind: "document",
|
|
1560
|
+
documentId: plan.documentId
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
if (command.action === "document.undelete" && Predicate.isTagged("Commit")(prepared)) for (const plan of [prepared.commitPlan, ...prepared.additionalPlans ?? []]) yield* accessControl.require({
|
|
1565
|
+
actor: activity.actor,
|
|
1566
|
+
requestId: activity.requestId
|
|
1567
|
+
}, {
|
|
1568
|
+
action: "document.undelete",
|
|
1569
|
+
resource: {
|
|
1570
|
+
kind: "document",
|
|
1571
|
+
documentId: plan.documentId
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
if (Predicate.isTagged("Unchanged")(prepared)) return {
|
|
1575
|
+
kind: "unchanged",
|
|
1576
|
+
result: prepared.result
|
|
1577
|
+
};
|
|
1578
|
+
const committed = yield* dependencies.repository.commit({
|
|
1579
|
+
activity,
|
|
1580
|
+
plan: prepared.commitPlan,
|
|
1581
|
+
additionalPlans: prepared.additionalPlans,
|
|
1582
|
+
expectedGeneration: current.snapshot.generation,
|
|
1583
|
+
mediaReferences: [prepared.commitPlan, ...prepared.additionalPlans ?? []].flatMap((plan) => projectMediaReferences(prepared.authored, prepared.published, plan.documentId))
|
|
1584
|
+
});
|
|
1585
|
+
const next = makeDocumentState(prepared.authored, prepared.published, committed.snapshot);
|
|
1586
|
+
const entry = entryById(next, committed.mutation.documentId);
|
|
1587
|
+
return {
|
|
1588
|
+
kind: "committed",
|
|
1589
|
+
next,
|
|
1590
|
+
result: DocumentMutationResult.make(Object.assign({}, committed.mutation, entry === void 0 ? void 0 : { entry }))
|
|
1591
|
+
};
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
//#endregion
|
|
1595
|
+
//#region ../server/src/internal/documents/document-import.ts
|
|
1596
|
+
const importDocumentSources = Effect.fn("Documents.importSources")(function* (dependencies, input, activity) {
|
|
1597
|
+
const report = {
|
|
1598
|
+
created: 0,
|
|
1599
|
+
preserved: 0,
|
|
1600
|
+
published: 0,
|
|
1601
|
+
unchanged: 0,
|
|
1602
|
+
updated: 0
|
|
1603
|
+
};
|
|
1604
|
+
for (const source of input.sources) {
|
|
1605
|
+
const plan = planDocumentImport(yield* dependencies.current(source.sourcePath), source, input.mode);
|
|
1606
|
+
switch (plan.kind) {
|
|
1607
|
+
case "create": {
|
|
1608
|
+
const created = yield* dependencies.runMutation({
|
|
1609
|
+
action: "document.create",
|
|
1610
|
+
documentId: yield* dependencies.nextDocumentId,
|
|
1611
|
+
markdown: source.markdown,
|
|
1612
|
+
sourcePath: source.sourcePath
|
|
1613
|
+
}, activity, input.reason);
|
|
1614
|
+
report.created += 1;
|
|
1615
|
+
if (plan.publish) {
|
|
1616
|
+
yield* publish(dependencies, created, input, activity);
|
|
1617
|
+
report.published += 1;
|
|
1618
|
+
}
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
case "preserve":
|
|
1622
|
+
case "unchanged":
|
|
1623
|
+
report[plan.kind === "preserve" ? "preserved" : "unchanged"] += 1;
|
|
1624
|
+
break;
|
|
1625
|
+
case "publish":
|
|
1626
|
+
yield* publish(dependencies, plan.current, input, activity);
|
|
1627
|
+
report.published += 1;
|
|
1628
|
+
break;
|
|
1629
|
+
case "update": {
|
|
1630
|
+
const updated = yield* dependencies.runMutation({
|
|
1631
|
+
action: "document.update",
|
|
1632
|
+
input: {
|
|
1633
|
+
documentId: plan.current.documentId,
|
|
1634
|
+
expectedVersion: plan.current.version,
|
|
1635
|
+
markdown: source.markdown,
|
|
1636
|
+
reason: input.reason
|
|
1637
|
+
}
|
|
1638
|
+
}, activity, input.reason);
|
|
1639
|
+
report.updated += 1;
|
|
1640
|
+
if (plan.publish) {
|
|
1641
|
+
yield* publish(dependencies, updated, input, activity);
|
|
1642
|
+
report.published += 1;
|
|
1643
|
+
}
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return DocumentImportReport.make(report);
|
|
1649
|
+
});
|
|
1650
|
+
function planDocumentImport(current, source, mode) {
|
|
1651
|
+
if (!current) return {
|
|
1652
|
+
kind: "create",
|
|
1653
|
+
publish: source.published
|
|
1654
|
+
};
|
|
1655
|
+
if (current.markdown !== source.markdown) return mode === "overwrite" ? {
|
|
1656
|
+
current,
|
|
1657
|
+
kind: "update",
|
|
1658
|
+
publish: source.published
|
|
1659
|
+
} : { kind: "preserve" };
|
|
1660
|
+
if (source.published && current.publishedVersion !== current.version) return {
|
|
1661
|
+
current,
|
|
1662
|
+
kind: "publish"
|
|
1663
|
+
};
|
|
1664
|
+
return { kind: "unchanged" };
|
|
1665
|
+
}
|
|
1666
|
+
function publish(dependencies, source, input, activity) {
|
|
1667
|
+
return dependencies.runMutation({
|
|
1668
|
+
action: "document.publish",
|
|
1669
|
+
input: {
|
|
1670
|
+
documentId: source.documentId,
|
|
1671
|
+
expectedVersion: source.version,
|
|
1672
|
+
reason: input.reason
|
|
1673
|
+
}
|
|
1674
|
+
}, activity, input.reason);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
//#endregion
|
|
1678
|
+
//#region ../server/src/internal/documents/document-store.ts
|
|
1679
|
+
var DocumentStore = class extends Context.Service()("@internal/server/DocumentStore") {};
|
|
1680
|
+
|
|
1681
|
+
//#endregion
|
|
1682
|
+
//#region ../server/src/internal/documents/document-failures.ts
|
|
1683
|
+
var DocumentPersistenceError = class extends Schema.TaggedError()("Documents.PersistenceError", {
|
|
1684
|
+
operation: Schema.NonEmptyString,
|
|
1685
|
+
cause: Schema.Defect()
|
|
1686
|
+
}) {};
|
|
1687
|
+
var DocumentGenerationConflict = class extends Schema.TaggedError()("Documents.GenerationConflict", {
|
|
1688
|
+
expectedGeneration: DocumentGeneration,
|
|
1689
|
+
actualGeneration: DocumentGeneration
|
|
1690
|
+
}) {};
|
|
1691
|
+
|
|
1692
|
+
//#endregion
|
|
1693
|
+
//#region ../server/src/internal/cursor.ts
|
|
1694
|
+
function cursorCodec(schema, makeCursor, invalid) {
|
|
1695
|
+
const decodeJson = Schema.decodeUnknownResult(schema);
|
|
1696
|
+
const encodeJson = flow(Schema.encodeResult(schema), Result.getOrThrow);
|
|
1697
|
+
return {
|
|
1698
|
+
decode: (cursor) => {
|
|
1699
|
+
const decoded = Encoding.decodeBase64UrlString(cursor);
|
|
1700
|
+
if (Result.isFailure(decoded)) return Result.fail(invalid());
|
|
1701
|
+
return decodeJson(decoded.success).pipe(Result.mapError(invalid));
|
|
1702
|
+
},
|
|
1703
|
+
encode: (payload) => makeCursor(Encoding.encodeBase64Url(encodeJson(payload)))
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region ../server/src/internal/documents/document-history.ts
|
|
1709
|
+
const CursorPayload = Schema.Struct({
|
|
1710
|
+
createdAt: Schema.DateTimeUtcFromString,
|
|
1711
|
+
revisionId: RevisionId
|
|
1712
|
+
});
|
|
1713
|
+
const CursorJson = Schema.fromJsonString(CursorPayload);
|
|
1714
|
+
const cursors = cursorCodec(CursorJson, (value) => DocumentHistoryCursor.make(value), () => new DocumentHistoryInvalidCursor());
|
|
1715
|
+
function documentHistoryCapability(accessControl, repository) {
|
|
1716
|
+
const requireHistory = (context, documentId) => accessControl.require(context, {
|
|
1717
|
+
action: "document.history",
|
|
1718
|
+
resource: {
|
|
1719
|
+
documentId,
|
|
1720
|
+
kind: "document"
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
const read = Effect.fn("Documents.history.read")(function* (context, documentId, revisionId) {
|
|
1724
|
+
yield* requireHistory(context, documentId);
|
|
1725
|
+
return yield* repository.revision(documentId, revisionId);
|
|
1726
|
+
});
|
|
1727
|
+
const list = Effect.fn("Documents.history.list")(function* (context, query) {
|
|
1728
|
+
yield* requireHistory(context, query.documentId);
|
|
1729
|
+
const cursor = query.cursor ? yield* Effect.fromResult(cursors.decode(query.cursor)) : void 0;
|
|
1730
|
+
const page = yield* repository.revisions({
|
|
1731
|
+
cursor,
|
|
1732
|
+
documentId: query.documentId,
|
|
1733
|
+
limit: query.limit ?? 25
|
|
1734
|
+
});
|
|
1735
|
+
const output = {
|
|
1736
|
+
revisions: page.revisions.map(({ markdown: _markdown, ...revision }) => revision),
|
|
1737
|
+
total: page.total
|
|
1738
|
+
};
|
|
1739
|
+
return page.next ? DocumentRevisionPage.make({
|
|
1740
|
+
...output,
|
|
1741
|
+
nextCursor: cursors.encode(page.next)
|
|
1742
|
+
}) : DocumentRevisionPage.make(output);
|
|
1743
|
+
});
|
|
1744
|
+
return {
|
|
1745
|
+
compare: Effect.fn("Documents.history.compare")(function* (context, input) {
|
|
1746
|
+
yield* requireHistory(context, input.documentId);
|
|
1747
|
+
const from = yield* repository.revision(input.documentId, input.fromRevisionId);
|
|
1748
|
+
const to = yield* repository.revision(input.documentId, input.toRevisionId);
|
|
1749
|
+
const changes = yield* compareLines(from.markdown, to.markdown);
|
|
1750
|
+
return DocumentRevisionComparison.make({
|
|
1751
|
+
changes,
|
|
1752
|
+
from,
|
|
1753
|
+
to
|
|
1754
|
+
});
|
|
1755
|
+
}),
|
|
1756
|
+
list,
|
|
1757
|
+
read
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
const MAXIMUM_COMPARISON_LINES = 1e4;
|
|
1761
|
+
const MAXIMUM_EDIT_LENGTH = 2e3;
|
|
1762
|
+
const comparisonTooLarge = () => new DocumentHistoryComparisonTooLarge({
|
|
1763
|
+
maximumEditLength: MAXIMUM_EDIT_LENGTH,
|
|
1764
|
+
maximumLines: MAXIMUM_COMPARISON_LINES
|
|
1765
|
+
});
|
|
1766
|
+
const compareLines = Effect.fn("Documents.history.compareLines")(function* (beforeMarkdown, afterMarkdown) {
|
|
1767
|
+
const before = beforeMarkdown.split("\n");
|
|
1768
|
+
const after = afterMarkdown.split("\n");
|
|
1769
|
+
if (before.length > MAXIMUM_COMPARISON_LINES || after.length > MAXIMUM_COMPARISON_LINES) return yield* comparisonTooLarge();
|
|
1770
|
+
const diff = diffArrays(before, after, {
|
|
1771
|
+
maxEditLength: MAXIMUM_EDIT_LENGTH,
|
|
1772
|
+
oneChangePerToken: true,
|
|
1773
|
+
timeout: 100
|
|
1774
|
+
});
|
|
1775
|
+
if (!diff) return yield* comparisonTooLarge();
|
|
1776
|
+
const changes = [];
|
|
1777
|
+
let beforeLine = 1;
|
|
1778
|
+
let afterLine = 1;
|
|
1779
|
+
for (const change of diff) for (const line of change.value) if (change.removed) {
|
|
1780
|
+
changes.push({
|
|
1781
|
+
beforeLine,
|
|
1782
|
+
kind: "removed",
|
|
1783
|
+
line
|
|
1784
|
+
});
|
|
1785
|
+
beforeLine += 1;
|
|
1786
|
+
} else if (change.added) {
|
|
1787
|
+
changes.push({
|
|
1788
|
+
afterLine,
|
|
1789
|
+
kind: "added",
|
|
1790
|
+
line
|
|
1791
|
+
});
|
|
1792
|
+
afterLine += 1;
|
|
1793
|
+
} else {
|
|
1794
|
+
changes.push({
|
|
1795
|
+
afterLine,
|
|
1796
|
+
beforeLine,
|
|
1797
|
+
kind: "equal",
|
|
1798
|
+
line
|
|
1799
|
+
});
|
|
1800
|
+
beforeLine += 1;
|
|
1801
|
+
afterLine += 1;
|
|
1802
|
+
}
|
|
1803
|
+
return changes;
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
//#endregion
|
|
1807
|
+
//#region ../server/src/internal/documents/document-delivery.ts
|
|
1808
|
+
function documentDelivery(configuration, catalog) {
|
|
1809
|
+
const read = Effect.fn("Documents.delivery.read")(function* (lookup) {
|
|
1810
|
+
const document = findDocument(yield* catalog, lookup);
|
|
1811
|
+
return Option.isSome(document) ? document.value : yield* new PublishedDocumentNotFound({ lookup });
|
|
1812
|
+
});
|
|
1813
|
+
const query = Effect.fn("Documents.delivery.query")(function* (input) {
|
|
1814
|
+
return queryDocuments((yield* catalog).documents, input, {
|
|
1815
|
+
limit: input.limit,
|
|
1816
|
+
offset: input.offset ?? 0
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
const navigation = Effect.fn("Documents.delivery.navigation")(function* (input) {
|
|
1820
|
+
return createNavigation((yield* catalog).documents, input, configuration);
|
|
1821
|
+
});
|
|
1822
|
+
const surround = Effect.fn("Documents.delivery.surround")(function* (lookup, input) {
|
|
1823
|
+
const current = yield* catalog;
|
|
1824
|
+
const document = findDocument(current, lookup);
|
|
1825
|
+
if (Option.isNone(document)) return yield* new PublishedDocumentNotFound({ lookup });
|
|
1826
|
+
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 });
|
|
1827
|
+
const page = queryDocuments(current.documents, surroundQuery);
|
|
1828
|
+
const index = page.items.findIndex((candidate) => candidate.id === document.value.id);
|
|
1829
|
+
return DocumentSurround.make({
|
|
1830
|
+
previous: index > 0 ? page.items[index - 1] ?? null : null,
|
|
1831
|
+
next: index >= 0 ? page.items[index + 1] ?? null : null
|
|
1832
|
+
});
|
|
1833
|
+
});
|
|
1834
|
+
return {
|
|
1835
|
+
diagnostics: catalog.pipe(Effect.map((current) => current.issues)),
|
|
1836
|
+
navigation,
|
|
1837
|
+
query,
|
|
1838
|
+
read,
|
|
1839
|
+
surround
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
function findDocument(catalog, lookup) {
|
|
1843
|
+
switch (lookup.kind) {
|
|
1844
|
+
case "id": return Option.fromNullishOr(catalog.documents.find((document) => document.id === lookup.documentId));
|
|
1845
|
+
case "path": return catalog.findByPath(lookup.path);
|
|
1846
|
+
case "key": return catalog.findByKey(lookup.key, lookup.locale).pipe(Option.filter((document) => lookup.collection === void 0 || document.collection === lookup.collection));
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
function queryDocuments(documents, query, page) {
|
|
1850
|
+
const filtered = documents.filter((document) => {
|
|
1851
|
+
if (query.collection !== void 0 && document.collection !== query.collection) return false;
|
|
1852
|
+
if (query.locale !== void 0 && document.locale !== query.locale) return false;
|
|
1853
|
+
return Object.entries(query.where ?? {}).every(([field, expected]) => matchesValue(documentField(document, field), expected));
|
|
1854
|
+
});
|
|
1855
|
+
const sorted = query.sort && query.sort.length > 0 ? sortDocuments(filtered, query.sort) : filtered;
|
|
1856
|
+
const items = page ? sorted.slice(page.offset, page.offset + page.limit) : sorted;
|
|
1857
|
+
return PublishedDocumentPage.make({
|
|
1858
|
+
items,
|
|
1859
|
+
total: filtered.length
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
function createNavigation(documents, query, configuration) {
|
|
1863
|
+
const locale = query.locale ?? configuration.locales?.defaultLocale;
|
|
1864
|
+
const candidates = documents.filter((document) => (query.collection === void 0 || document.collection === query.collection) && (locale === void 0 || document.locale === locale) && document.data.navigation !== false);
|
|
1865
|
+
const roots = [];
|
|
1866
|
+
const itemsByKey = /* @__PURE__ */ new Map();
|
|
1867
|
+
const orderByItem = /* @__PURE__ */ new Map();
|
|
1868
|
+
for (const document of candidates) {
|
|
1869
|
+
if (document.key === "/") {
|
|
1870
|
+
const item = navigationItem(document);
|
|
1871
|
+
roots.push(item);
|
|
1872
|
+
orderByItem.set(item, Number.NEGATIVE_INFINITY);
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const segments = document.key.slice(1).split("/");
|
|
1876
|
+
let parent;
|
|
1877
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
1878
|
+
const key = PublicPath.make(`/${segments.slice(0, index + 1).join("/")}`);
|
|
1879
|
+
let item = itemsByKey.get(key);
|
|
1880
|
+
if (!item) {
|
|
1881
|
+
item = {
|
|
1882
|
+
children: [],
|
|
1883
|
+
key,
|
|
1884
|
+
title: titleFromSegment(segments[index] ?? "")
|
|
1885
|
+
};
|
|
1886
|
+
itemsByKey.set(key, item);
|
|
1887
|
+
if (parent) parent.children.push(item);
|
|
1888
|
+
else roots.push(item);
|
|
1889
|
+
}
|
|
1890
|
+
if (index === segments.length - 1) {
|
|
1891
|
+
const resolved = navigationItem(document);
|
|
1892
|
+
item.title = resolved.title;
|
|
1893
|
+
item.path = resolved.path;
|
|
1894
|
+
if (resolved.locale !== void 0) item.locale = resolved.locale;
|
|
1895
|
+
else delete item.locale;
|
|
1896
|
+
item.documentId = resolved.documentId;
|
|
1897
|
+
orderByItem.set(item, navigationOrder(document));
|
|
1898
|
+
}
|
|
1899
|
+
parent = item;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
sortNavigation(roots, orderByItem);
|
|
1903
|
+
return roots.map((item) => DocumentNavigationItem.make(item));
|
|
1904
|
+
}
|
|
1905
|
+
function navigationItem(document) {
|
|
1906
|
+
const navigation = document.data.navigation;
|
|
1907
|
+
const configuredTitle = Schema.is(Schema.JsonObject)(navigation) ? navigation.title : void 0;
|
|
1908
|
+
return Object.assign({
|
|
1909
|
+
children: [],
|
|
1910
|
+
documentId: document.id,
|
|
1911
|
+
key: document.key,
|
|
1912
|
+
path: document.path,
|
|
1913
|
+
title: Schema.is(Schema.String)(configuredTitle) && configuredTitle.trim() ? configuredTitle.trim() : document.title
|
|
1914
|
+
}, document.locale === void 0 ? void 0 : { locale: document.locale });
|
|
1915
|
+
}
|
|
1916
|
+
function navigationOrder(document) {
|
|
1917
|
+
const navigation = document.data.navigation;
|
|
1918
|
+
const order = (Schema.is(Schema.JsonObject)(navigation) ? navigation.order : void 0) ?? document.data.order;
|
|
1919
|
+
return Schema.is(Schema.Finite)(order) ? order : Number.POSITIVE_INFINITY;
|
|
1920
|
+
}
|
|
1921
|
+
function sortNavigation(items, orderByItem) {
|
|
1922
|
+
items.sort((left, right) => {
|
|
1923
|
+
return (orderByItem.get(left) ?? Number.POSITIVE_INFINITY) - (orderByItem.get(right) ?? Number.POSITIVE_INFINITY) || left.title.localeCompare(right.title);
|
|
1924
|
+
});
|
|
1925
|
+
items.forEach((item) => sortNavigation(item.children, orderByItem));
|
|
1926
|
+
}
|
|
1927
|
+
function documentField(document, field) {
|
|
1928
|
+
switch (field) {
|
|
1929
|
+
case "collection": return document.collection;
|
|
1930
|
+
case "description": return document.description;
|
|
1931
|
+
case "id": return document.id;
|
|
1932
|
+
case "key": return document.key;
|
|
1933
|
+
case "locale": return document.locale;
|
|
1934
|
+
case "path": return document.path;
|
|
1935
|
+
case "source.path": return document.source.path;
|
|
1936
|
+
case "title": return document.title;
|
|
1937
|
+
default: return nestedValue(document.data, field.startsWith("data.") ? field.slice(5) : field);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function nestedValue(value, path) {
|
|
1941
|
+
return path.split(".").reduce((current, segment) => {
|
|
1942
|
+
if (!Schema.is(Schema.JsonObject)(current)) return void 0;
|
|
1943
|
+
return current[segment];
|
|
1944
|
+
}, value);
|
|
1945
|
+
}
|
|
1946
|
+
function matchesValue(actual, expected) {
|
|
1947
|
+
const expectedValues = Array.isArray(expected) ? expected : [expected];
|
|
1948
|
+
return (Array.isArray(actual) ? actual : [actual]).some((value) => Schema.is(DocumentQueryScalar)(value) && expectedValues.includes(value));
|
|
1949
|
+
}
|
|
1950
|
+
function sortDocuments(documents, sorts) {
|
|
1951
|
+
return documents.toSorted((left, right) => {
|
|
1952
|
+
for (const sort of sorts) {
|
|
1953
|
+
const comparison = compareValues(documentField(left, sort.field), documentField(right, sort.field));
|
|
1954
|
+
if (comparison !== 0) return sort.direction === "desc" ? -comparison : comparison;
|
|
1955
|
+
}
|
|
1956
|
+
return 0;
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
function compareValues(left, right) {
|
|
1960
|
+
if (left === right) return 0;
|
|
1961
|
+
if (left === void 0 || left === null) return 1;
|
|
1962
|
+
if (right === void 0 || right === null) return -1;
|
|
1963
|
+
if (Schema.is(Schema.Finite)(left) && Schema.is(Schema.Finite)(right)) return left - right;
|
|
1964
|
+
const leftText = Schema.is(Schema.String)(left) ? left : JSON.stringify(left) ?? "";
|
|
1965
|
+
const rightText = Schema.is(Schema.String)(right) ? right : JSON.stringify(right) ?? "";
|
|
1966
|
+
return leftText.localeCompare(rightText);
|
|
1967
|
+
}
|
|
1968
|
+
function titleFromSegment(segment) {
|
|
1969
|
+
return segment.replace(/[-_]+/gu, " ").replace(/^./u, (character) => character.toUpperCase());
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
//#endregion
|
|
1973
|
+
//#region ../server/src/internal/documents/documents.ts
|
|
1974
|
+
const isDocumentGenerationConflict = Schema.is(DocumentGenerationConflict);
|
|
1975
|
+
const isDocumentMediaUnavailable = Schema.is(DocumentMediaUnavailable);
|
|
1976
|
+
const CONTENT_MUTATION_BUFFER_CAPACITY = 16;
|
|
1977
|
+
var Documents = class extends Context.Service()("@internal/server/Documents") {};
|
|
1978
|
+
var DocumentPreviews = class extends Context.Service()("@internal/server/DocumentPreviews") {};
|
|
1979
|
+
var PublishedDocuments = class extends Context.Service()("@internal/server/PublishedDocuments") {};
|
|
1980
|
+
var DocumentMaintenance = class extends Context.Service()("@internal/server/internal/DocumentMaintenance") {};
|
|
1981
|
+
const makeDocuments = Effect.fn("Documents.make")(function* (configuration, options) {
|
|
1982
|
+
const repository = yield* DocumentStore;
|
|
1983
|
+
const accessControl = yield* AccessControl;
|
|
1984
|
+
const crypto = yield* Crypto.Crypto;
|
|
1985
|
+
const engine = yield* createDocumentEngine(configuration, options);
|
|
1986
|
+
const state = yield* Ref.make(yield* loadSynchronizedState(repository, engine));
|
|
1987
|
+
const semaphore = yield* Semaphore.make(1);
|
|
1988
|
+
const mutationEvents = yield* Effect.acquireRelease(PubSub.sliding(CONTENT_MUTATION_BUFFER_CAPACITY), PubSub.shutdown);
|
|
1989
|
+
const dependencies = {
|
|
1990
|
+
engine,
|
|
1991
|
+
configuration,
|
|
1992
|
+
repository,
|
|
1993
|
+
components: options.components
|
|
1994
|
+
};
|
|
1995
|
+
const refreshUnlocked = Effect.fn("Documents.refreshUnlocked")(function* (force = false) {
|
|
1996
|
+
const current = yield* Ref.get(state);
|
|
1997
|
+
const snapshot = force ? yield* loadRequired(repository) : yield* repository.load(current.snapshot.generation);
|
|
1998
|
+
if (!snapshot || !generationIsAfter(snapshot.generation, current.snapshot.generation)) return current.snapshot.generation;
|
|
1999
|
+
const refreshed = yield* compileDocumentState(engine, snapshot, current);
|
|
2000
|
+
yield* Ref.set(state, refreshed);
|
|
2001
|
+
return refreshed.snapshot.generation;
|
|
2002
|
+
});
|
|
2003
|
+
const executeUnlocked = Effect.fn("Documents.execute")(function* (command, activity) {
|
|
2004
|
+
return yield* Effect.gen(function* () {
|
|
2005
|
+
yield* refreshUnlocked();
|
|
2006
|
+
const current = yield* Ref.get(state);
|
|
2007
|
+
const outcome = yield* commitDocumentMutation(accessControl, dependencies, current, command, activity);
|
|
2008
|
+
if (outcome.kind === "unchanged") return outcome.result;
|
|
2009
|
+
yield* Ref.set(state, outcome.next);
|
|
2010
|
+
yield* PubSub.publish(mutationEvents, outcome.result);
|
|
2011
|
+
return outcome.result;
|
|
2012
|
+
}).pipe(Effect.tapError((error) => isDocumentGenerationConflict(error) ? refreshUnlocked(true) : Effect.void), Effect.retry({
|
|
2013
|
+
schedule: Schedule.recurs(3),
|
|
2014
|
+
while: isDocumentGenerationConflict
|
|
2015
|
+
}));
|
|
2016
|
+
});
|
|
2017
|
+
const runMutation = Effect.fn("Documents.runMutation")(function* (command, activity, reason) {
|
|
2018
|
+
const resolvedActivity = enrichActivity(activity, reason);
|
|
2019
|
+
return yield* semaphore.withPermits(1)(executeUnlocked(command, resolvedActivity));
|
|
2020
|
+
});
|
|
2021
|
+
const authored = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2022
|
+
yield* refreshUnlocked();
|
|
2023
|
+
return (yield* Ref.get(state)).authored;
|
|
2024
|
+
})).pipe(Effect.withSpan("Documents.authored"));
|
|
2025
|
+
const browse = Effect.fn("Documents.browse")((query) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2026
|
+
yield* refreshUnlocked();
|
|
2027
|
+
return authoringEntries(yield* Ref.get(state), query);
|
|
2028
|
+
})));
|
|
2029
|
+
const create = Effect.fn("Documents.create")(function* (input, activity) {
|
|
2030
|
+
const collection = configuration.types.find((candidate) => candidate.collection === input.collection);
|
|
2031
|
+
if (collection && !collection.canCreate) return yield* new DocumentValidationFailed({ issues: [{
|
|
2032
|
+
code: "document.invalid",
|
|
2033
|
+
field: "collection",
|
|
2034
|
+
message: `Document collection "${input.collection}" does not allow creation.`,
|
|
2035
|
+
severity: "error"
|
|
2036
|
+
}] });
|
|
2037
|
+
const plan = yield* Effect.fromResult(planDocumentCreation(configuration, input));
|
|
2038
|
+
const source = yield* createInitialDocumentSource(plan);
|
|
2039
|
+
const documentId = DocumentId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
2040
|
+
return yield* runMutation({
|
|
2041
|
+
action: "document.create",
|
|
2042
|
+
documentId,
|
|
2043
|
+
markdown: source.markdown,
|
|
2044
|
+
sourcePath: plan.sourcePath
|
|
2045
|
+
}, activity, input.reason);
|
|
2046
|
+
});
|
|
2047
|
+
const importSources = (input, activity) => importDocumentSources({
|
|
2048
|
+
current: (sourcePath) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2049
|
+
yield* refreshUnlocked();
|
|
2050
|
+
return (yield* Ref.get(state)).snapshot.active.find((candidate) => candidate.sourcePath === sourcePath);
|
|
2051
|
+
})),
|
|
2052
|
+
nextDocumentId: crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((value) => DocumentId.make(value))),
|
|
2053
|
+
runMutation
|
|
2054
|
+
}, input, activity);
|
|
2055
|
+
const duplicate = Effect.fn("Documents.duplicate")((input, activity) => runMutation({
|
|
2056
|
+
action: "document.duplicate",
|
|
2057
|
+
input,
|
|
2058
|
+
nextDocumentId: crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((value) => DocumentId.make(value)))
|
|
2059
|
+
}, activity, input.reason));
|
|
2060
|
+
const unpublish = Effect.fn("Documents.unpublish")((input, activity) => runMutation({
|
|
2061
|
+
action: "document.unpublish",
|
|
2062
|
+
input
|
|
2063
|
+
}, activity, input.reason));
|
|
2064
|
+
const move = Effect.fn("Documents.move")((input, activity) => runMutation({
|
|
2065
|
+
action: "document.move",
|
|
2066
|
+
input
|
|
2067
|
+
}, activity, input.reason));
|
|
2068
|
+
const subscribeMutations = Effect.gen(function* () {
|
|
2069
|
+
const subscription = yield* PubSub.subscribe(mutationEvents);
|
|
2070
|
+
const baseline = yield* semaphore.withPermits(1)(Ref.get(state).pipe(Effect.map((current) => current.snapshot.generation)));
|
|
2071
|
+
return {
|
|
2072
|
+
baseline,
|
|
2073
|
+
mutations: Stream.fromSubscription(subscription).pipe(Stream.filter((mutation) => generationIsAfter(mutation.generation, baseline)))
|
|
2074
|
+
};
|
|
2075
|
+
}).pipe(Effect.withSpan("Documents.subscribeMutations"));
|
|
2076
|
+
const publish = Effect.fn("Documents.publish")((input, activity) => runMutation({
|
|
2077
|
+
action: "document.publish",
|
|
2078
|
+
input
|
|
2079
|
+
}, activity, input.reason));
|
|
2080
|
+
const published = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2081
|
+
yield* refreshUnlocked();
|
|
2082
|
+
return (yield* Ref.get(state)).published;
|
|
2083
|
+
})).pipe(Effect.withSpan("Documents.published"));
|
|
2084
|
+
const read = Effect.fn("Documents.read")((documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2085
|
+
yield* refreshUnlocked();
|
|
2086
|
+
return (yield* Ref.get(state)).activeById.get(documentId) || (yield* new DocumentNotFound({ documentId }));
|
|
2087
|
+
})));
|
|
2088
|
+
const refresh = semaphore.withPermits(1)(refreshUnlocked()).pipe(Effect.withSpan("Documents.refresh"));
|
|
2089
|
+
const remove = Effect.fn("Documents.remove")((input, activity) => runMutation({
|
|
2090
|
+
action: "document.remove",
|
|
2091
|
+
input
|
|
2092
|
+
}, activity, input.reason));
|
|
2093
|
+
const restore = Effect.fn("Documents.restore")((input, activity) => runMutation({
|
|
2094
|
+
action: "document.restore",
|
|
2095
|
+
input
|
|
2096
|
+
}, activity, input.reason));
|
|
2097
|
+
const trash = semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2098
|
+
yield* refreshUnlocked();
|
|
2099
|
+
return deletedEntries(yield* Ref.get(state));
|
|
2100
|
+
})).pipe(Effect.withSpan("Documents.trash"));
|
|
2101
|
+
const undelete = Effect.fn("Documents.undelete")((input, activity) => runMutation({
|
|
2102
|
+
action: "document.undelete",
|
|
2103
|
+
input
|
|
2104
|
+
}, activity, input.reason));
|
|
2105
|
+
const update = Effect.fn("Documents.update")((input, activity) => runMutation({
|
|
2106
|
+
action: "document.update",
|
|
2107
|
+
input
|
|
2108
|
+
}, activity, input.reason));
|
|
2109
|
+
const validate = Effect.fn("Documents.validate")((input) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2110
|
+
yield* refreshUnlocked();
|
|
2111
|
+
return yield* validateDocumentUpdate(engine, yield* Ref.get(state), input);
|
|
2112
|
+
})));
|
|
2113
|
+
const history = documentHistoryCapability(accessControl, repository);
|
|
2114
|
+
const delivery = documentDelivery(configuration, published);
|
|
2115
|
+
const document = (documentId) => ({
|
|
2116
|
+
documentId,
|
|
2117
|
+
kind: "document"
|
|
2118
|
+
});
|
|
2119
|
+
const requireThen = (context, action, resource, effect) => accessControl.require(context, {
|
|
2120
|
+
action,
|
|
2121
|
+
resource
|
|
2122
|
+
}).pipe(Effect.andThen(effect));
|
|
2123
|
+
const documents = Documents.of({
|
|
2124
|
+
browse: (context, query) => requireThen(context, "document.browse", query.collection ? {
|
|
2125
|
+
collection: query.collection,
|
|
2126
|
+
kind: "collection",
|
|
2127
|
+
locale: query.locale
|
|
2128
|
+
} : { kind: "catalog" }, () => browse(query)),
|
|
2129
|
+
create: (context, input) => requireThen(context, "document.create", {
|
|
2130
|
+
collection: input.collection,
|
|
2131
|
+
kind: "collection",
|
|
2132
|
+
locale: input.locale
|
|
2133
|
+
}, () => create(input, requestActivity$1(context, input.reason))),
|
|
2134
|
+
historyCompare: history.compare,
|
|
2135
|
+
historyList: history.list,
|
|
2136
|
+
historyRead: history.read,
|
|
2137
|
+
duplicate: (context, input) => requireThen(context, "document.duplicate", document(input.documentId), () => duplicate(input, requestActivity$1(context, input.reason))),
|
|
2138
|
+
unpublish: (context, input) => requireThen(context, "document.unpublish", document(input.documentId), () => unpublish(input, requestActivity$1(context, input.reason))),
|
|
2139
|
+
move: (context, input) => requireThen(context, "document.move", document(input.documentId), () => move(input, requestActivity$1(context, input.reason))),
|
|
2140
|
+
publish: (context, input) => requireThen(context, "document.publish", document(input.documentId), () => publish(input, requestActivity$1(context, input.reason))),
|
|
2141
|
+
read: (context, documentId) => requireThen(context, "document.read", document(documentId), () => read(documentId)),
|
|
2142
|
+
remove: (context, input) => requireThen(context, "document.remove", document(input.documentId), () => remove(input, requestActivity$1(context, input.reason))),
|
|
2143
|
+
restore: (context, input) => requireThen(context, "document.restore", document(input.documentId), () => restore(input, requestActivity$1(context, input.reason))),
|
|
2144
|
+
trash: (context) => requireThen(context, "document.trash", { kind: "catalog" }, () => trash),
|
|
2145
|
+
undelete: (context, input) => requireThen(context, "document.undelete", document(input.documentId), () => undelete(input, requestActivity$1(context, input.reason))),
|
|
2146
|
+
update: (context, input) => requireThen(context, "document.update", document(input.documentId), () => update(input, requestActivity$1(context, input.reason))),
|
|
2147
|
+
validate: (context, input) => requireThen(context, "document.validate", document(input.documentId), () => validate(input))
|
|
2148
|
+
});
|
|
2149
|
+
const draftDelivery = documentDelivery(configuration, authored);
|
|
2150
|
+
const previews = DocumentPreviews.of({
|
|
2151
|
+
read: (context, lookup) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.read(lookup)),
|
|
2152
|
+
query: (context, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.query(query)),
|
|
2153
|
+
navigation: (context, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.navigation(query)),
|
|
2154
|
+
surround: (context, lookup, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.surround(lookup, query))
|
|
2155
|
+
});
|
|
2156
|
+
const publishedService = PublishedDocuments.of({
|
|
2157
|
+
navigation: delivery.navigation,
|
|
2158
|
+
query: delivery.query,
|
|
2159
|
+
read: delivery.read,
|
|
2160
|
+
snapshot: published.pipe(Effect.map((catalog) => catalog.documents)),
|
|
2161
|
+
surround: delivery.surround,
|
|
2162
|
+
diagnostics: (context) => accessControl.require(context, {
|
|
2163
|
+
action: "document.browse",
|
|
2164
|
+
resource: { kind: "catalog" }
|
|
2165
|
+
}).pipe(Effect.andThen(delivery.diagnostics))
|
|
2166
|
+
});
|
|
2167
|
+
return {
|
|
2168
|
+
documents,
|
|
2169
|
+
maintenance: DocumentMaintenance.of({
|
|
2170
|
+
importSources,
|
|
2171
|
+
mediaReferences: (documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
2172
|
+
yield* refreshUnlocked();
|
|
2173
|
+
const current = yield* Ref.get(state);
|
|
2174
|
+
return projectMediaReferences(current.authored, current.published, documentId);
|
|
2175
|
+
})),
|
|
2176
|
+
publish,
|
|
2177
|
+
read,
|
|
2178
|
+
refresh,
|
|
2179
|
+
subscribeMutations,
|
|
2180
|
+
validate
|
|
2181
|
+
}),
|
|
2182
|
+
previews,
|
|
2183
|
+
publishedDocuments: publishedService
|
|
2184
|
+
};
|
|
2185
|
+
});
|
|
2186
|
+
function documentsLayer(configuration, options) {
|
|
2187
|
+
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)))));
|
|
2188
|
+
}
|
|
2189
|
+
function loadRequired(repository) {
|
|
2190
|
+
return repository.load().pipe(Effect.filterOrFail((snapshot) => snapshot !== void 0, () => new DocumentPersistenceError({
|
|
2191
|
+
cause: /* @__PURE__ */ new Error("DocumentStore returned no initial snapshot."),
|
|
2192
|
+
operation: "DocumentStore.load"
|
|
2193
|
+
})));
|
|
2194
|
+
}
|
|
2195
|
+
function loadSynchronizedState(repository, engine) {
|
|
2196
|
+
return Effect.gen(function* () {
|
|
2197
|
+
const snapshot = yield* loadRequired(repository);
|
|
2198
|
+
const state = yield* compileDocumentState(engine, snapshot);
|
|
2199
|
+
yield* repository.synchronizeMediaReferences({
|
|
2200
|
+
expectedGeneration: snapshot.generation,
|
|
2201
|
+
references: projectMediaReferences(state.authored, state.published)
|
|
2202
|
+
});
|
|
2203
|
+
return state;
|
|
2204
|
+
}).pipe(Effect.retry({
|
|
2205
|
+
schedule: Schedule.recurs(3),
|
|
2206
|
+
while: isDocumentGenerationConflict
|
|
2207
|
+
}), Effect.mapError((error) => isDocumentGenerationConflict(error) || isDocumentMediaUnavailable(error) ? new DocumentPersistenceError({
|
|
2208
|
+
cause: error,
|
|
2209
|
+
operation: "Documents.synchronizeMediaReferences"
|
|
2210
|
+
}) : error));
|
|
2211
|
+
}
|
|
2212
|
+
function generationIsAfter(candidate, current) {
|
|
2213
|
+
return BigInt(candidate) > BigInt(current);
|
|
2214
|
+
}
|
|
2215
|
+
function enrichActivity(activity, reason) {
|
|
2216
|
+
return ActivityContext.make(Object.assign({}, activity, reason === void 0 ? void 0 : { reason }));
|
|
2217
|
+
}
|
|
2218
|
+
function requestActivity$1(context, reason) {
|
|
2219
|
+
return ActivityContext.make(Object.assign({
|
|
2220
|
+
actor: context.actor,
|
|
2221
|
+
requestId: context.requestId
|
|
2222
|
+
}, reason === void 0 ? void 0 : { reason }));
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
//#endregion
|
|
2226
|
+
//#region ../server/src/internal/seo/seo-store.ts
|
|
2227
|
+
var SeoPersistenceError = class extends Schema.TaggedError()("Seo.PersistenceError", {
|
|
2228
|
+
operation: Schema.NonEmptyString,
|
|
2229
|
+
cause: Schema.Defect()
|
|
2230
|
+
}) {};
|
|
2231
|
+
var SeoStore = class extends Context.Service()("@internal/server/SeoStore") {};
|
|
2232
|
+
|
|
2233
|
+
//#endregion
|
|
2234
|
+
//#region ../server/src/internal/seo/seo.ts
|
|
2235
|
+
var Seo = class extends Context.Service()("@internal/server/Seo") {};
|
|
2236
|
+
var PublishedSeo = class extends Context.Service()("@internal/server/PublishedSeo") {};
|
|
2237
|
+
var PreviewSeo = class extends Context.Service()("@internal/server/PreviewSeo") {};
|
|
2238
|
+
var SeoDelivery = class extends Context.Service()("@internal/server/SeoDelivery") {};
|
|
2239
|
+
function makeDefaultSeoOptions(baseUrl) {
|
|
2240
|
+
const url = new URL(baseUrl);
|
|
2241
|
+
return {
|
|
2242
|
+
capabilities: SeoCapabilities.make({
|
|
2243
|
+
generatedSocialImageTemplateIds: [],
|
|
2244
|
+
identityTypes: [
|
|
2245
|
+
"Person",
|
|
2246
|
+
"Organization",
|
|
2247
|
+
"LocalBusiness"
|
|
2248
|
+
],
|
|
2249
|
+
structuredDataTypes: ["WebPage", "Article"]
|
|
2250
|
+
}),
|
|
2251
|
+
defaults: GlobalSeoSettings.make({
|
|
2252
|
+
canonicalOrigin: SeoCanonicalOrigin.make(url.origin),
|
|
2253
|
+
follow: true,
|
|
2254
|
+
index: true,
|
|
2255
|
+
siteName: url.hostname
|
|
2256
|
+
})
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
const seoLayer = (options) => Layer.effectContext(Effect.gen(function* () {
|
|
2260
|
+
const accessControl = yield* AccessControl;
|
|
2261
|
+
const store = yield* SeoStore;
|
|
2262
|
+
const load = Effect.fn("Seo.load")(function* () {
|
|
2263
|
+
return (yield* store.load) ?? (yield* store.initialize(options.defaults));
|
|
2264
|
+
});
|
|
2265
|
+
const state = Effect.fn("Seo.state")(function* () {
|
|
2266
|
+
return makeState(yield* load(), options.capabilities);
|
|
2267
|
+
});
|
|
2268
|
+
const requireAdministrator = (context, action) => accessControl.require(context, {
|
|
2269
|
+
action,
|
|
2270
|
+
resource: { kind: "seo" }
|
|
2271
|
+
});
|
|
2272
|
+
const read = Effect.fn("Seo.read")(function* (context) {
|
|
2273
|
+
yield* requireAdministrator(context, "seo.read");
|
|
2274
|
+
return yield* state();
|
|
2275
|
+
});
|
|
2276
|
+
const save = Effect.fn("Seo.save")(function* (context, input) {
|
|
2277
|
+
yield* requireAdministrator(context, "seo.save");
|
|
2278
|
+
const snapshot = yield* store.save({
|
|
2279
|
+
activity: requestActivity(context, input.reason),
|
|
2280
|
+
expectedVersion: input.expectedVersion,
|
|
2281
|
+
settings: input.settings
|
|
2282
|
+
});
|
|
2283
|
+
return SeoMutationResult.make({
|
|
2284
|
+
action: "seo.save",
|
|
2285
|
+
state: makeState(snapshot, options.capabilities)
|
|
2286
|
+
});
|
|
2287
|
+
});
|
|
2288
|
+
const publish = Effect.fn("Seo.publish")(function* (context, input) {
|
|
2289
|
+
yield* requireAdministrator(context, "seo.publish");
|
|
2290
|
+
const snapshot = yield* load();
|
|
2291
|
+
const confirmationChanges = productionChanges(snapshot.published?.settings ?? options.defaults, snapshot.working.settings);
|
|
2292
|
+
if (confirmationChanges.length > 0 && input.confirmedSiteWideChange !== true) return yield* new SeoConfirmationRequired({ changes: confirmationChanges });
|
|
2293
|
+
const published = yield* store.publish({
|
|
2294
|
+
activity: requestActivity(context, input.reason),
|
|
2295
|
+
expectedVersion: input.expectedVersion
|
|
2296
|
+
});
|
|
2297
|
+
return SeoMutationResult.make({
|
|
2298
|
+
action: "seo.publish",
|
|
2299
|
+
state: makeState(published, options.capabilities)
|
|
2300
|
+
});
|
|
2301
|
+
});
|
|
2302
|
+
const seo = Seo.of({
|
|
2303
|
+
publish,
|
|
2304
|
+
read,
|
|
2305
|
+
save
|
|
2306
|
+
});
|
|
2307
|
+
const publishedSeo = PublishedSeo.of({ read: load().pipe(Effect.map((snapshot) => SeoSnapshot.make({
|
|
2308
|
+
capabilities: options.capabilities,
|
|
2309
|
+
revision: snapshot.published ?? snapshot.working
|
|
2310
|
+
}))) });
|
|
2311
|
+
const previewSeo = PreviewSeo.of({ read: load().pipe(Effect.map((snapshot) => SeoSnapshot.make({
|
|
2312
|
+
capabilities: options.capabilities,
|
|
2313
|
+
revision: snapshot.working
|
|
2314
|
+
}))) });
|
|
2315
|
+
return Context.empty().pipe(Context.add(Seo, seo), Context.add(PublishedSeo, publishedSeo), Context.add(PreviewSeo, previewSeo));
|
|
2316
|
+
}));
|
|
2317
|
+
const seoDeliveryLayer = Layer.effect(SeoDelivery, Effect.gen(function* () {
|
|
2318
|
+
const previews = yield* PreviewSeo;
|
|
2319
|
+
const published = yield* PublishedSeo;
|
|
2320
|
+
const documents = yield* PublishedDocuments;
|
|
2321
|
+
const resolveWith = (document, snapshot) => CompiledDocument.make(Object.assign({}, document, { resolvedSeo: resolveDocumentSeo({
|
|
2322
|
+
capabilities: snapshot.capabilities,
|
|
2323
|
+
document,
|
|
2324
|
+
global: snapshot.revision.settings
|
|
2325
|
+
}) }));
|
|
2326
|
+
const publishedDocument = Effect.fn("SeoDelivery.publishedDocument")((document) => published.read.pipe(Effect.map((snapshot) => resolveWith(document, snapshot))));
|
|
2327
|
+
const previewDocument = Effect.fn("SeoDelivery.previewDocument")((document) => previews.read.pipe(Effect.map((snapshot) => resolveWith(document, snapshot))));
|
|
2328
|
+
const sitemap = Effect.gen(function* () {
|
|
2329
|
+
const snapshot = yield* published.read;
|
|
2330
|
+
const resolved = (yield* documents.snapshot).flatMap((document) => {
|
|
2331
|
+
const resolvedDocument = resolveWith(document, snapshot);
|
|
2332
|
+
return resolvedDocument.resolvedSeo?.index === true && resolvedDocument.source.publishedAt !== void 0 ? [resolvedDocument] : [];
|
|
2333
|
+
});
|
|
2334
|
+
const localesByKey = /* @__PURE__ */ new Map();
|
|
2335
|
+
for (const document of resolved) {
|
|
2336
|
+
if (document.locale === void 0) continue;
|
|
2337
|
+
const alternatives = localesByKey.get(document.key) ?? [];
|
|
2338
|
+
alternatives.push({
|
|
2339
|
+
locale: document.locale,
|
|
2340
|
+
loc: document.path
|
|
2341
|
+
});
|
|
2342
|
+
localesByKey.set(document.key, alternatives);
|
|
2343
|
+
}
|
|
2344
|
+
return resolved.flatMap((document) => {
|
|
2345
|
+
const publishedAt = document.source.publishedAt;
|
|
2346
|
+
if (publishedAt === void 0) return [];
|
|
2347
|
+
const alternatives = localesByKey.get(document.key);
|
|
2348
|
+
const entry = {
|
|
2349
|
+
loc: document.path,
|
|
2350
|
+
lastmod: publishedAt
|
|
2351
|
+
};
|
|
2352
|
+
return [SitemapEntry.make(alternatives && alternatives.length > 1 ? {
|
|
2353
|
+
...entry,
|
|
2354
|
+
alternatives
|
|
2355
|
+
} : entry)];
|
|
2356
|
+
}).sort((left, right) => left.loc.localeCompare(right.loc));
|
|
2357
|
+
}).pipe(Effect.withSpan("SeoDelivery.sitemap"));
|
|
2358
|
+
return SeoDelivery.of({
|
|
2359
|
+
previewDocument,
|
|
2360
|
+
publishedDocument,
|
|
2361
|
+
sitemap
|
|
2362
|
+
});
|
|
2363
|
+
}));
|
|
2364
|
+
function makeState(snapshot, capabilities) {
|
|
2365
|
+
return SeoState.make(snapshot.published === void 0 ? {
|
|
2366
|
+
capabilities,
|
|
2367
|
+
working: snapshot.working
|
|
2368
|
+
} : {
|
|
2369
|
+
capabilities,
|
|
2370
|
+
published: snapshot.published,
|
|
2371
|
+
working: snapshot.working
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
function requestActivity(context, reason) {
|
|
2375
|
+
return {
|
|
2376
|
+
actor: context.actor,
|
|
2377
|
+
requestId: context.requestId,
|
|
2378
|
+
...reason === void 0 ? void 0 : { reason }
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
function productionChanges(previous, next) {
|
|
2382
|
+
const changes = [];
|
|
2383
|
+
if (previous.canonicalOrigin !== next.canonicalOrigin) changes.push("canonicalOrigin");
|
|
2384
|
+
if (previous.index !== next.index || previous.follow !== next.follow) changes.push("indexability");
|
|
2385
|
+
return changes;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
//#endregion
|
|
2389
|
+
//#region ../server/src/internal/media/media-object-store.ts
|
|
2390
|
+
const mediaObjectPrefix = "media/";
|
|
2391
|
+
var MediaObjectStore = class extends Context.Service()("@internal/server/MediaObjectStore") {};
|
|
2392
|
+
|
|
2393
|
+
//#endregion
|
|
2394
|
+
//#region ../server/src/internal/media/media-failures.ts
|
|
2395
|
+
var MediaPersistenceError = class extends Schema.TaggedError()("Media.PersistenceError", {
|
|
2396
|
+
operation: Schema.NonEmptyString,
|
|
2397
|
+
cause: Schema.Defect()
|
|
2398
|
+
}) {};
|
|
2399
|
+
var MediaStorageError = class extends Schema.TaggedError()("Media.StorageError", {
|
|
2400
|
+
operation: Schema.NonEmptyString,
|
|
2401
|
+
cause: Schema.Defect()
|
|
2402
|
+
}) {};
|
|
2403
|
+
|
|
2404
|
+
//#endregion
|
|
2405
|
+
//#region ../s3/src/internal/client.ts
|
|
2406
|
+
var S3RequestError = class extends Schema.TaggedError()("S3.RequestError", {
|
|
2407
|
+
cause: Schema.Defect(),
|
|
2408
|
+
operation: Schema.NonEmptyString
|
|
2409
|
+
}) {};
|
|
2410
|
+
function openAwsS3MediaObjectClient(options) {
|
|
2411
|
+
const credentials = options.credentials ? {
|
|
2412
|
+
accessKeyId: options.credentials.accessKeyId,
|
|
2413
|
+
secretAccessKey: Redacted.value(options.credentials.secretAccessKey),
|
|
2414
|
+
sessionToken: options.credentials.sessionToken ? Redacted.value(options.credentials.sessionToken) : void 0
|
|
2415
|
+
} : void 0;
|
|
2416
|
+
return makeAwsS3MediaObjectClient(new S3Client({
|
|
2417
|
+
credentials,
|
|
2418
|
+
endpoint: options.endpoint,
|
|
2419
|
+
forcePathStyle: options.forcePathStyle,
|
|
2420
|
+
region: options.region,
|
|
2421
|
+
requestChecksumCalculation: "WHEN_REQUIRED"
|
|
2422
|
+
}));
|
|
2423
|
+
}
|
|
2424
|
+
function makeAwsS3MediaObjectClient(client) {
|
|
2425
|
+
const request = (operation, send) => Effect.tryPromise({
|
|
2426
|
+
catch: (cause) => new S3RequestError({
|
|
2427
|
+
cause,
|
|
2428
|
+
operation
|
|
2429
|
+
}),
|
|
2430
|
+
try: send
|
|
2431
|
+
});
|
|
2432
|
+
return {
|
|
2433
|
+
close: Effect.sync(() => client.destroy()),
|
|
2434
|
+
copyObject: (input) => request("MediaObjectStore.S3.copyObject", (signal) => client.send(new CopyObjectCommand({
|
|
2435
|
+
Bucket: input.bucket,
|
|
2436
|
+
CacheControl: input.cacheControl,
|
|
2437
|
+
ContentType: input.contentType,
|
|
2438
|
+
CopySource: input.copySource,
|
|
2439
|
+
Key: input.key,
|
|
2440
|
+
MetadataDirective: "REPLACE"
|
|
2441
|
+
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
2442
|
+
createBucket: (input) => request("MediaObjectStore.S3.createBucket", (signal) => client.send(new CreateBucketCommand({
|
|
2443
|
+
Bucket: input.bucket,
|
|
2444
|
+
CreateBucketConfiguration: input.locationConstraint ? { LocationConstraint: input.locationConstraint } : void 0
|
|
2445
|
+
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
2446
|
+
deleteObject: (input) => request("MediaObjectStore.S3.deleteObject", (signal) => client.send(new DeleteObjectCommand({
|
|
2447
|
+
Bucket: input.bucket,
|
|
2448
|
+
Key: input.key
|
|
2449
|
+
}), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
2450
|
+
headBucket: (input) => request("MediaObjectStore.S3.headBucket", (signal) => client.send(new HeadBucketCommand({ Bucket: input.bucket }), { abortSignal: signal })).pipe(Effect.asVoid),
|
|
2451
|
+
headObject: (input) => request("MediaObjectStore.S3.headObject", (signal) => client.send(new HeadObjectCommand({
|
|
2452
|
+
Bucket: input.bucket,
|
|
2453
|
+
Key: input.key
|
|
2454
|
+
}), { abortSignal: signal })).pipe(Effect.map((output) => ({
|
|
2455
|
+
contentLength: output.ContentLength,
|
|
2456
|
+
contentType: output.ContentType
|
|
2457
|
+
}))),
|
|
2458
|
+
listObjects: (input) => request("MediaObjectStore.S3.listObjects", (signal) => client.send(new ListObjectsV2Command({
|
|
2459
|
+
Bucket: input.bucket,
|
|
2460
|
+
ContinuationToken: input.continuationToken,
|
|
2461
|
+
Prefix: input.prefix
|
|
2462
|
+
}), { abortSignal: signal })).pipe(Effect.map((output) => ({
|
|
2463
|
+
isTruncated: output.IsTruncated === true,
|
|
2464
|
+
nextContinuationToken: output.NextContinuationToken,
|
|
2465
|
+
objects: (output.Contents ?? []).map((object) => ({
|
|
2466
|
+
key: object.Key,
|
|
2467
|
+
size: object.Size
|
|
2468
|
+
}))
|
|
2469
|
+
}))),
|
|
2470
|
+
putObject: (input) => request("MediaObjectStore.S3.putObject", (signal) => client.send(new PutObjectCommand({
|
|
2471
|
+
Body: NodeStream.toReadableNever(input.body),
|
|
2472
|
+
Bucket: input.bucket,
|
|
2473
|
+
CacheControl: input.cacheControl,
|
|
2474
|
+
ContentLength: input.contentLength,
|
|
2475
|
+
ContentType: input.contentType,
|
|
2476
|
+
Key: input.key
|
|
2477
|
+
}), { abortSignal: signal })).pipe(Effect.asVoid)
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
//#endregion
|
|
2482
|
+
//#region ../s3/src/internal/media-object-store.ts
|
|
2483
|
+
const replaceableMediaCacheControl = "public, max-age=0, must-revalidate";
|
|
2484
|
+
const isWriteBodyError = Schema.is(Schema.Union([MediaInvalid, MediaStorageError]));
|
|
2485
|
+
function s3MediaObjectStoreLayer(options) {
|
|
2486
|
+
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))))));
|
|
2487
|
+
}
|
|
2488
|
+
function makeStore(options, client) {
|
|
2489
|
+
return {
|
|
2490
|
+
list: listObjects(options.bucket, client),
|
|
2491
|
+
promote: (input) => promoteObject(options.bucket, input, client),
|
|
2492
|
+
put: (input) => putObject(options.bucket, input, client),
|
|
2493
|
+
remove: (objectKey) => providerRequest("MediaObjectStore.S3.remove", client.deleteObject({
|
|
2494
|
+
bucket: options.bucket,
|
|
2495
|
+
key: objectKey
|
|
2496
|
+
})),
|
|
2497
|
+
stat: (objectKey) => statObject(options.bucket, objectKey, client)
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function promoteObject(bucket, input, client) {
|
|
2501
|
+
if (input.byteSize > 5e9) return Effect.fail(storageError("MediaObjectStore.S3.promote", "The object exceeds the S3 single-copy size limit."));
|
|
2502
|
+
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({
|
|
2503
|
+
bucket,
|
|
2504
|
+
cacheControl: replaceableMediaCacheControl,
|
|
2505
|
+
contentType: input.mimeType,
|
|
2506
|
+
copySource: copySource(bucket, input.sourceObjectKey),
|
|
2507
|
+
key: input.destinationObjectKey
|
|
2508
|
+
}))), 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."))));
|
|
2509
|
+
}
|
|
2510
|
+
function copySource(bucket, objectKey) {
|
|
2511
|
+
return `/${encodeURIComponent(bucket)}/${objectKey.split("/").map(encodeURIComponent).join("/")}`;
|
|
2512
|
+
}
|
|
2513
|
+
function putObject(bucket, input, client) {
|
|
2514
|
+
return Effect.gen(function* () {
|
|
2515
|
+
const body = yield* validatedWriteBody(input);
|
|
2516
|
+
yield* client.putObject({
|
|
2517
|
+
body,
|
|
2518
|
+
bucket,
|
|
2519
|
+
cacheControl: replaceableMediaCacheControl,
|
|
2520
|
+
contentLength: input.byteSize,
|
|
2521
|
+
contentType: input.mimeType,
|
|
2522
|
+
key: input.objectKey
|
|
2523
|
+
}).pipe(Effect.catch((failure) => isWriteBodyError(failure.cause) ? Effect.fail(failure.cause) : Effect.fail(providerError("MediaObjectStore.S3.put", failure))));
|
|
2524
|
+
return {
|
|
2525
|
+
byteSize: input.byteSize,
|
|
2526
|
+
mimeType: input.mimeType,
|
|
2527
|
+
objectKey: input.objectKey
|
|
2528
|
+
};
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
const validatedWriteBody = Effect.fn("MediaObjectStore.S3.validateWriteBody")(function* (input) {
|
|
2532
|
+
const byteLength = yield* Ref.make(0);
|
|
2533
|
+
return input.body.pipe(Stream.mapEffect((chunk) => Effect.gen(function* () {
|
|
2534
|
+
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.");
|
|
2535
|
+
return chunk;
|
|
2536
|
+
})), 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)));
|
|
2537
|
+
});
|
|
2538
|
+
function statObject(bucket, objectKey, client) {
|
|
2539
|
+
return client.headObject({
|
|
2540
|
+
bucket,
|
|
2541
|
+
key: objectKey
|
|
2542
|
+
}).pipe(Effect.matchEffect({
|
|
2543
|
+
onFailure: (failure) => isMissingObject(failure.cause) ? Effect.succeed(Option.none()) : Effect.fail(providerError("MediaObjectStore.S3.stat", failure)),
|
|
2544
|
+
onSuccess: (output) => decodeStoredObject("MediaObjectStore.S3.stat", objectKey, output.contentLength, output.contentType).pipe(Effect.asSome)
|
|
2545
|
+
}));
|
|
2546
|
+
}
|
|
2547
|
+
function listObjects(bucket, client) {
|
|
2548
|
+
return Stream.paginate({ seenTokens: /* @__PURE__ */ new Set() }, (state) => providerRequest("MediaObjectStore.S3.list", client.listObjects({
|
|
2549
|
+
bucket,
|
|
2550
|
+
continuationToken: state.continuationToken,
|
|
2551
|
+
prefix: mediaObjectPrefix
|
|
2552
|
+
})).pipe(Effect.flatMap((page) => Effect.gen(function* () {
|
|
2553
|
+
return [yield* Effect.forEach(page.objects, decodeListedObject), yield* nextListState(state, page)];
|
|
2554
|
+
}))));
|
|
2555
|
+
}
|
|
2556
|
+
function decodeListedObject(object) {
|
|
2557
|
+
if (object.key === void 0) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned object metadata without a key."));
|
|
2558
|
+
return decodeStoredObject("MediaObjectStore.S3.list", object.key, object.size);
|
|
2559
|
+
}
|
|
2560
|
+
function nextListState(state, page) {
|
|
2561
|
+
if (!page.isTruncated) return Effect.succeedNone;
|
|
2562
|
+
const token = page.nextContinuationToken;
|
|
2563
|
+
if (!token || token === state.continuationToken || state.seenTokens.has(token)) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned an invalid continuation token."));
|
|
2564
|
+
return Effect.succeedSome({
|
|
2565
|
+
continuationToken: token,
|
|
2566
|
+
seenTokens: /* @__PURE__ */ new Set([...state.seenTokens, token])
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
function decodeStoredObject(operation, objectKey, byteSize, contentType) {
|
|
2570
|
+
const decodedObjectKey = Schema.decodeOption(MediaObjectKey)(objectKey);
|
|
2571
|
+
const decodedByteSize = Schema.decodeUnknownOption(MediaByteSize)(byteSize);
|
|
2572
|
+
if (Option.isNone(decodedObjectKey) || Option.isNone(decodedByteSize)) return Effect.fail(storageError(operation, "The provider returned invalid object metadata."));
|
|
2573
|
+
const decodedMimeType = Schema.decodeUnknownOption(ImageMimeType)(contentType);
|
|
2574
|
+
const storedObject = {
|
|
2575
|
+
byteSize: decodedByteSize.value,
|
|
2576
|
+
objectKey: decodedObjectKey.value
|
|
2577
|
+
};
|
|
2578
|
+
return Effect.succeed(Option.isSome(decodedMimeType) ? {
|
|
2579
|
+
...storedObject,
|
|
2580
|
+
mimeType: decodedMimeType.value
|
|
2581
|
+
} : storedObject);
|
|
2582
|
+
}
|
|
2583
|
+
function providerRequest(operation, request) {
|
|
2584
|
+
return request.pipe(Effect.mapError((failure) => providerError(operation, failure)));
|
|
2585
|
+
}
|
|
2586
|
+
function provisionBucket(options, client) {
|
|
2587
|
+
if (!options.provisionBucket) return Effect.void;
|
|
2588
|
+
return client.headBucket({ bucket: options.bucket }).pipe(Effect.catch((failure) => isMissingBucket(failure.cause) ? createMissingBucket(options, client) : Effect.fail(providerError("MediaObjectStore.S3.bucket.head", failure))));
|
|
2589
|
+
}
|
|
2590
|
+
function createMissingBucket(options, client) {
|
|
2591
|
+
return client.createBucket({
|
|
2592
|
+
bucket: options.bucket,
|
|
2593
|
+
locationConstraint: options.endpoint || options.region === "us-east-1" ? void 0 : options.region
|
|
2594
|
+
}).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))));
|
|
2595
|
+
}
|
|
2596
|
+
const ProviderFailureDetails = Schema.Struct({
|
|
2597
|
+
$metadata: Schema.optionalKey(Schema.Struct({
|
|
2598
|
+
attempts: Schema.optionalKey(Schema.Finite),
|
|
2599
|
+
httpStatusCode: Schema.optionalKey(Schema.Finite)
|
|
2600
|
+
})),
|
|
2601
|
+
name: Schema.optionalKey(Schema.String)
|
|
2602
|
+
});
|
|
2603
|
+
const decodeProviderFailure = Schema.decodeUnknownOption(ProviderFailureDetails);
|
|
2604
|
+
function providerError(operation, failure) {
|
|
2605
|
+
const details = decodeProviderFailure(failure.cause);
|
|
2606
|
+
const diagnostics = [];
|
|
2607
|
+
if (Option.isSome(details)) {
|
|
2608
|
+
if (details.value.name) diagnostics.push(`name=${details.value.name}`);
|
|
2609
|
+
if (details.value.$metadata?.httpStatusCode !== void 0) diagnostics.push(`status=${details.value.$metadata.httpStatusCode}`);
|
|
2610
|
+
if (details.value.$metadata?.attempts !== void 0) diagnostics.push(`attempts=${details.value.$metadata.attempts}`);
|
|
2611
|
+
}
|
|
2612
|
+
return storageError(operation, `The S3 provider request failed${diagnostics.length > 0 ? ` (${diagnostics.join(", ")}).` : "."}`);
|
|
2613
|
+
}
|
|
2614
|
+
function isMissingObject(cause) {
|
|
2615
|
+
const details = decodeProviderFailure(cause);
|
|
2616
|
+
if (Option.isNone(details)) return false;
|
|
2617
|
+
return details.value.name === "NoSuchKey" || details.value.name === "NotFound";
|
|
2618
|
+
}
|
|
2619
|
+
function isMissingBucket(cause) {
|
|
2620
|
+
const details = decodeProviderFailure(cause);
|
|
2621
|
+
if (Option.isNone(details)) return false;
|
|
2622
|
+
return details.value.name === "NoSuchBucket" || details.value.$metadata?.httpStatusCode === 404;
|
|
2623
|
+
}
|
|
2624
|
+
function isBucketAlreadyOwned(cause) {
|
|
2625
|
+
const details = decodeProviderFailure(cause);
|
|
2626
|
+
return Option.isSome(details) && details.value.name === "BucketAlreadyOwnedByYou";
|
|
2627
|
+
}
|
|
2628
|
+
function resolveOptions(options) {
|
|
2629
|
+
const bucket = options.bucket.trim();
|
|
2630
|
+
const region = options.region.trim();
|
|
2631
|
+
const endpoint = options.endpoint?.trim();
|
|
2632
|
+
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."));
|
|
2633
|
+
if (options.credentials) {
|
|
2634
|
+
const secretAccessKey = Redacted.value(options.credentials.secretAccessKey);
|
|
2635
|
+
const sessionToken = options.credentials.sessionToken && Redacted.value(options.credentials.sessionToken);
|
|
2636
|
+
if (!options.credentials.accessKeyId.trim() || !secretAccessKey || sessionToken === "") return Effect.fail(storageError("MediaObjectStore.S3.configure", "The configured credentials must be non-empty."));
|
|
2637
|
+
}
|
|
2638
|
+
return Effect.succeed({
|
|
2639
|
+
...options,
|
|
2640
|
+
bucket,
|
|
2641
|
+
credentials: options.credentials ? {
|
|
2642
|
+
...options.credentials,
|
|
2643
|
+
accessKeyId: options.credentials.accessKeyId.trim()
|
|
2644
|
+
} : void 0,
|
|
2645
|
+
endpoint,
|
|
2646
|
+
forcePathStyle: options.forcePathStyle ?? false,
|
|
2647
|
+
provisionBucket: options.provisionBucket ?? false,
|
|
2648
|
+
region
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
function storageError(operation, message) {
|
|
2652
|
+
return new MediaStorageError({
|
|
2653
|
+
cause: new Error(message),
|
|
2654
|
+
operation
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
//#endregion
|
|
2659
|
+
//#region ../s3/src/S3MediaObjects.ts
|
|
2660
|
+
const maxS3SingleCopyBytes = 5e9;
|
|
2661
|
+
|
|
2662
|
+
//#endregion
|
|
2663
|
+
export { DocumentRevision as $, StudioBadRequest as A, User as B, DocumentStore as C, CurrentStudioRequest as D, defaultAccessControlLayer as E, studioInternalError as F, UserSelfRemovalDenied as G, UserAlreadyExists as H, InitializationAlreadyCompleted as I, CollaborationServerMessage as J, UserSelfRoleChangeDenied as K, InitializationProofInvalid as L, StudioRequestConflict as M, StudioSchemaError as N, StudioApi as O, StudioInternalError as P, DocumentHistoryInvalidCursor as Q, InitializationState as R, DocumentPersistenceError as S, AccessControl as T, UserNotFound as U, UserAdministrationDenied as V, UserPage as W, StateVector as X, CollaborationTicket as Y, DocumentHistoryComparisonTooLarge as Z, Documents as _, mediaObjectPrefix as a, ActivityEntry as at, cursorCodec as b, Seo as c, ActivityPage as ct, seoDeliveryLayer as d, DocumentMediaUnavailable as dt, ACTIVITY_ACTIONS as et, seoLayer as f, DocumentNotFound as ft, DocumentPreviews as g, PublishedDocumentNotFound as gt, DocumentMaintenance as h, DocumentVersionConflict as ht, MediaObjectStore as i, ActivityCursorInvalid as it, StudioBootstrap as j, StudioAuthentication as k, SeoDelivery as l, ActivityTarget as lt, SeoStore as m, DocumentValidationFailed as mt, s3MediaObjectStoreLayer as n, ActivityContext as nt, PreviewSeo as o, ActivityEntryDraft as ot, SeoPersistenceError as p, DocumentPathConflict as pt, CollaborationClientMessage as q, MediaPersistenceError as r, ActivityCursor as rt, PublishedSeo as s, ActivityId as st, maxS3SingleCopyBytes as t, ActivityAction as tt, makeDefaultSeoOptions as u, ActivityTargetKind as ut, PublishedDocuments as v, renderEditorMarkdown as w, DocumentGenerationConflict as x, documentsLayer as y, CreateUserInput as z };
|
|
2664
|
+
//# sourceMappingURL=S3MediaObjects-JKMAWyVB.js.map
|