@crombolo/server 0.0.0-stage → 0.9.1
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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/package.json +41 -4
- package/src/access.ts +114 -0
- package/src/adapters/assistant.ts +229 -0
- package/src/adapters/blueprints.ts +163 -0
- package/src/adapters/bundles.ts +432 -0
- package/src/adapters/content.ts +783 -0
- package/src/adapters/dictionary.ts +76 -0
- package/src/adapters/documents.ts +637 -0
- package/src/adapters/elements.ts +403 -0
- package/src/adapters/file-system.ts +195 -0
- package/src/adapters/media.ts +347 -0
- package/src/adapters/member-groups.ts +90 -0
- package/src/adapters/members.ts +261 -0
- package/src/adapters/public-access.ts +116 -0
- package/src/adapters/redirects.ts +73 -0
- package/src/adapters/references.ts +67 -0
- package/src/adapters/schema-files.ts +204 -0
- package/src/adapters/schema-proposals.ts +180 -0
- package/src/adapters/template-files.ts +58 -0
- package/src/adapters/template-query.ts +127 -0
- package/src/adapters/users.ts +963 -0
- package/src/assistant.ts +487 -0
- package/src/backup.ts +52 -0
- package/src/bundles.ts +508 -0
- package/src/component-snippets.ts +163 -0
- package/src/components.ts +266 -0
- package/src/config.ts +548 -0
- package/src/content-type-udt.ts +256 -0
- package/src/database.ts +109 -0
- package/src/diagnostics.ts +143 -0
- package/src/dictionary-transfer.ts +123 -0
- package/src/domains.ts +384 -0
- package/src/editor-types.ts +108 -0
- package/src/email-http.ts +79 -0
- package/src/email-notice.ts +36 -0
- package/src/email-postmark.ts +78 -0
- package/src/email-resend.ts +69 -0
- package/src/email-ses.ts +307 -0
- package/src/email.ts +402 -0
- package/src/extensions-notice.ts +35 -0
- package/src/file-intake.ts +84 -0
- package/src/form-files.ts +241 -0
- package/src/form-submit.ts +397 -0
- package/src/form-workflows.ts +397 -0
- package/src/forms.ts +170 -0
- package/src/git-github.ts +171 -0
- package/src/git-routes.ts +154 -0
- package/src/git.ts +88 -0
- package/src/imaging.ts +434 -0
- package/src/index.ts +62 -0
- package/src/jobs.ts +140 -0
- package/src/listen.ts +36 -0
- package/src/log-expressions.ts +524 -0
- package/src/log-viewer.ts +190 -0
- package/src/logging.ts +126 -0
- package/src/marketplace.ts +297 -0
- package/src/media-files.ts +276 -0
- package/src/media-store-azure.ts +218 -0
- package/src/media-store-s3.ts +113 -0
- package/src/media-store.ts +248 -0
- package/src/member-auth.ts +384 -0
- package/src/models-builder.ts +106 -0
- package/src/no-nodes.ts +64 -0
- package/src/not-implemented.ts +44 -0
- package/src/oembed.ts +144 -0
- package/src/ports.ts +553 -0
- package/src/redirects.ts +132 -0
- package/src/schema-classify.ts +82 -0
- package/src/schema-store.ts +145 -0
- package/src/schema.ts +388 -0
- package/src/seo.ts +371 -0
- package/src/server-events.ts +131 -0
- package/src/server.ts +1579 -0
- package/src/status.ts +246 -0
- package/src/udt.ts +168 -0
- package/src/value-schemas.ts +64 -0
- package/src/view-runtime.ts +42 -0
- package/src/zip.ts +152 -0
- package/README.md +0 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template views on disk.
|
|
3
|
+
*
|
|
4
|
+
* Umbraco keeps templates at ~/components/{alias}.cshtml and treats the file as the
|
|
5
|
+
* source of truth for its content; we do the same with .tsx.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import type { ComponentFileStore } from '@crombolo/data'
|
|
10
|
+
|
|
11
|
+
/** Aliases become filenames, so they must not be able to escape the directory. */
|
|
12
|
+
function safeAlias(alias: string): string | undefined {
|
|
13
|
+
return /^[A-Za-z0-9_-]+$/.test(alias) ? alias : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `onChange` fires after a template is written or removed, with the alias.
|
|
18
|
+
*
|
|
19
|
+
* A view is code: the node that wrote it has to take a new snapshot before it
|
|
20
|
+
* renders again, and the others have to be told to look. This is the only place
|
|
21
|
+
* that knows a view changed.
|
|
22
|
+
*/
|
|
23
|
+
export function createTemplateFileStore(
|
|
24
|
+
dir: string,
|
|
25
|
+
onChange?: (alias: string) => void | Promise<void>,
|
|
26
|
+
): ComponentFileStore {
|
|
27
|
+
const fileFor = (alias: string): string | undefined => {
|
|
28
|
+
const safe = safeAlias(alias)
|
|
29
|
+
return safe ? join(dir, `${safe}.tsx`) : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
async read(alias) {
|
|
34
|
+
const file = fileFor(alias)
|
|
35
|
+
if (!file) return undefined
|
|
36
|
+
try {
|
|
37
|
+
return await readFile(file, 'utf8')
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async write(alias, content) {
|
|
43
|
+
const file = fileFor(alias)
|
|
44
|
+
if (!file) throw new Error(`Unsafe template alias '${alias}'.`)
|
|
45
|
+
await mkdir(dir, { recursive: true })
|
|
46
|
+
await writeFile(file, content, 'utf8')
|
|
47
|
+
// Awaited, so a saved template is already the one this node renders by the
|
|
48
|
+
// time the editor's request comes back.
|
|
49
|
+
await onChange?.(alias)
|
|
50
|
+
},
|
|
51
|
+
async remove(alias) {
|
|
52
|
+
const file = fileFor(alias)
|
|
53
|
+
if (!file) return
|
|
54
|
+
await rm(file, { force: true })
|
|
55
|
+
await onChange?.(alias)
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs what the template query builder describes.
|
|
3
|
+
*
|
|
4
|
+
* Against the published cache and through `Navigation`, because that is what a
|
|
5
|
+
* component renders from: the count and the sample the builder shows are then
|
|
6
|
+
* the answer the snippet it wrote would give, not a second implementation that
|
|
7
|
+
* can drift from it.
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
TemplateQueryFilter,
|
|
11
|
+
TemplateQueryPort,
|
|
12
|
+
TemplateQueryRequest,
|
|
13
|
+
TemplateQuerySort,
|
|
14
|
+
} from '@crombolo/api-management'
|
|
15
|
+
import type { ContentTypeRepository } from '@crombolo/data'
|
|
16
|
+
import type { PublishedCache, PublishedContent } from '@crombolo/render'
|
|
17
|
+
|
|
18
|
+
/** The accessor each queryable property names, and how it compares. */
|
|
19
|
+
const FIELDS: Record<
|
|
20
|
+
string,
|
|
21
|
+
{ of: (content: PublishedContent) => string | number; text: boolean }
|
|
22
|
+
> = {
|
|
23
|
+
Id: { of: (content) => content.id, text: false },
|
|
24
|
+
Name: { of: (content) => content.name, text: true },
|
|
25
|
+
CreateDate: { of: (content) => content.createDate.getTime(), text: false },
|
|
26
|
+
UpdateDate: { of: (content) => content.updateDate.getTime(), text: false },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matches(content: PublishedContent, filter: TemplateQueryFilter): boolean {
|
|
30
|
+
const field = FIELDS[filter.propertyAlias]
|
|
31
|
+
if (!field) return true
|
|
32
|
+
const left = field.of(content)
|
|
33
|
+
|
|
34
|
+
if (field.text) {
|
|
35
|
+
const right = filter.constraintValue
|
|
36
|
+
const text = String(left)
|
|
37
|
+
switch (filter.operator) {
|
|
38
|
+
case 'Equals':
|
|
39
|
+
return text === right
|
|
40
|
+
case 'NotEquals':
|
|
41
|
+
return text !== right
|
|
42
|
+
case 'Contains':
|
|
43
|
+
return text.includes(right)
|
|
44
|
+
case 'NotContains':
|
|
45
|
+
return !text.includes(right)
|
|
46
|
+
default:
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const right = filter.propertyAlias.endsWith('Date')
|
|
52
|
+
? new Date(filter.constraintValue).getTime()
|
|
53
|
+
: Number(filter.constraintValue)
|
|
54
|
+
// An unparseable constraint filters nothing out rather than everything: the
|
|
55
|
+
// builder sends what has been typed so far, half-finished dates included.
|
|
56
|
+
if (Number.isNaN(right)) return true
|
|
57
|
+
const value = Number(left)
|
|
58
|
+
switch (filter.operator) {
|
|
59
|
+
case 'Equals':
|
|
60
|
+
return value === right
|
|
61
|
+
case 'NotEquals':
|
|
62
|
+
return value !== right
|
|
63
|
+
case 'LessThan':
|
|
64
|
+
return value < right
|
|
65
|
+
case 'LessThanEqualTo':
|
|
66
|
+
return value <= right
|
|
67
|
+
case 'GreaterThan':
|
|
68
|
+
return value > right
|
|
69
|
+
case 'GreaterThanEqualTo':
|
|
70
|
+
return value >= right
|
|
71
|
+
default:
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function compare(sort: TemplateQuerySort): (a: PublishedContent, b: PublishedContent) => number {
|
|
77
|
+
const field = FIELDS[sort.propertyAlias]
|
|
78
|
+
if (!field) return () => 0
|
|
79
|
+
const descending = sort.direction?.toLowerCase() === 'descending'
|
|
80
|
+
return (a, b) => {
|
|
81
|
+
const [first, second] = descending ? [b, a] : [a, b]
|
|
82
|
+
const left = field.of(first)
|
|
83
|
+
const right = field.of(second)
|
|
84
|
+
if (field.text) return String(left).localeCompare(String(right))
|
|
85
|
+
return Number(left) - Number(right)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createTemplateQueryPort(options: {
|
|
90
|
+
cache: PublishedCache
|
|
91
|
+
types: ContentTypeRepository
|
|
92
|
+
}): TemplateQueryPort {
|
|
93
|
+
return {
|
|
94
|
+
async documentTypeAliases() {
|
|
95
|
+
const all = await options.types.all()
|
|
96
|
+
return (
|
|
97
|
+
all
|
|
98
|
+
// An element type is never routed to, so a query cannot return one.
|
|
99
|
+
.filter((type) => !type.isElement)
|
|
100
|
+
.map((type) => type.alias)
|
|
101
|
+
.sort((a, b) => a.localeCompare(b))
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async execute(request: TemplateQueryRequest) {
|
|
106
|
+
const nav = await options.cache.navigation()
|
|
107
|
+
const root = request.rootKey ? nav.byKey(request.rootKey) : undefined
|
|
108
|
+
// No root picked means "below the page this renders on", which has no one
|
|
109
|
+
// answer here — the site's own roots are the closest honest stand-in.
|
|
110
|
+
const from = root ? nav.children(root) : nav.root().flatMap((node) => nav.children(node))
|
|
111
|
+
|
|
112
|
+
let found = from
|
|
113
|
+
if (request.documentTypeAlias)
|
|
114
|
+
found = found.filter((item) => item.contentType.alias === request.documentTypeAlias)
|
|
115
|
+
for (const filter of request.filters) found = found.filter((item) => matches(item, filter))
|
|
116
|
+
if (request.sort) found = [...found].sort(compare(request.sort))
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
total: found.length,
|
|
120
|
+
items: found.slice(0, request.take).map((item) => ({
|
|
121
|
+
icon: 'icon-document',
|
|
122
|
+
name: item.name,
|
|
123
|
+
})),
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|