@crombolo/server 0.0.0-stage → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/package.json +41 -4
  4. package/src/access.ts +114 -0
  5. package/src/adapters/assistant.ts +229 -0
  6. package/src/adapters/blueprints.ts +163 -0
  7. package/src/adapters/bundles.ts +507 -0
  8. package/src/adapters/content.ts +783 -0
  9. package/src/adapters/dictionary.ts +76 -0
  10. package/src/adapters/documents.ts +637 -0
  11. package/src/adapters/elements.ts +403 -0
  12. package/src/adapters/file-system.ts +195 -0
  13. package/src/adapters/media.ts +347 -0
  14. package/src/adapters/member-groups.ts +90 -0
  15. package/src/adapters/members.ts +261 -0
  16. package/src/adapters/public-access.ts +116 -0
  17. package/src/adapters/redirects.ts +73 -0
  18. package/src/adapters/references.ts +67 -0
  19. package/src/adapters/schema-files.ts +204 -0
  20. package/src/adapters/schema-proposals.ts +180 -0
  21. package/src/adapters/template-files.ts +58 -0
  22. package/src/adapters/template-query.ts +127 -0
  23. package/src/adapters/users.ts +963 -0
  24. package/src/assistant.ts +487 -0
  25. package/src/backup.ts +52 -0
  26. package/src/bundle-install.ts +417 -0
  27. package/src/bundle-sections.ts +424 -0
  28. package/src/bundle-tables.ts +161 -0
  29. package/src/bundles.ts +609 -0
  30. package/src/component-snippets.ts +163 -0
  31. package/src/components.ts +266 -0
  32. package/src/config.ts +556 -0
  33. package/src/content-type-udt.ts +256 -0
  34. package/src/database.ts +148 -0
  35. package/src/diagnostics.ts +143 -0
  36. package/src/dictionary-transfer.ts +123 -0
  37. package/src/domains.ts +384 -0
  38. package/src/editor-types.ts +108 -0
  39. package/src/email-http.ts +79 -0
  40. package/src/email-notice.ts +36 -0
  41. package/src/email-postmark.ts +78 -0
  42. package/src/email-resend.ts +69 -0
  43. package/src/email-ses.ts +307 -0
  44. package/src/email.ts +402 -0
  45. package/src/extensions-notice.ts +35 -0
  46. package/src/file-intake.ts +84 -0
  47. package/src/form-files.ts +241 -0
  48. package/src/form-submit.ts +397 -0
  49. package/src/form-workflows.ts +397 -0
  50. package/src/forms.ts +170 -0
  51. package/src/git-github.ts +171 -0
  52. package/src/git-routes.ts +154 -0
  53. package/src/git.ts +88 -0
  54. package/src/imaging.ts +434 -0
  55. package/src/index.ts +65 -0
  56. package/src/jobs.ts +140 -0
  57. package/src/listen.ts +36 -0
  58. package/src/log-expressions.ts +524 -0
  59. package/src/log-viewer.ts +190 -0
  60. package/src/logging.ts +126 -0
  61. package/src/marketplace.ts +382 -0
  62. package/src/media-files.ts +276 -0
  63. package/src/media-store-azure.ts +218 -0
  64. package/src/media-store-s3.ts +113 -0
  65. package/src/media-store.ts +248 -0
  66. package/src/member-auth.ts +384 -0
  67. package/src/models-builder.ts +106 -0
  68. package/src/no-nodes.ts +64 -0
  69. package/src/not-implemented.ts +44 -0
  70. package/src/oembed.ts +144 -0
  71. package/src/ports.ts +553 -0
  72. package/src/redirects.ts +132 -0
  73. package/src/schema-classify.ts +82 -0
  74. package/src/schema-store.ts +145 -0
  75. package/src/schema.ts +388 -0
  76. package/src/seo.ts +371 -0
  77. package/src/server-events.ts +131 -0
  78. package/src/server.ts +1642 -0
  79. package/src/status.ts +275 -0
  80. package/src/tar.ts +85 -0
  81. package/src/udt.ts +168 -0
  82. package/src/value-schemas.ts +64 -0
  83. package/src/view-runtime.ts +42 -0
  84. package/src/zip.ts +152 -0
  85. 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
+ }