@crombolo/server 0.16.0 → 0.18.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.
- package/package.json +12 -12
- package/src/adapters/documents.ts +9 -2
- package/src/adapters/users.ts +45 -3
- package/src/approvals.ts +10 -0
- package/src/components.ts +77 -1
- package/src/config.ts +82 -81
- package/src/content-notifications.ts +310 -0
- package/src/email-ses.ts +4 -3
- package/src/email-text.ts +61 -0
- package/src/email.ts +1 -0
- package/src/form-files.ts +3 -0
- package/src/form-workflows.ts +159 -8
- package/src/index.ts +3 -0
- package/src/jobs.ts +36 -4
- package/src/ports.ts +33 -0
- package/src/server.ts +40 -1
- package/src/settings.ts +921 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "The crombolo HTTP server: routing, configuration, media stores and background jobs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Crombolo",
|
|
@@ -30,17 +30,17 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@crombolo/api-delivery": "0.
|
|
34
|
-
"@crombolo/api-management": "0.
|
|
35
|
-
"@crombolo/assistant": "0.
|
|
36
|
-
"@crombolo/auth": "0.
|
|
37
|
-
"@crombolo/backoffice-host": "0.
|
|
38
|
-
"@crombolo/contracts": "0.
|
|
39
|
-
"@crombolo/core": "0.
|
|
40
|
-
"@crombolo/data": "0.
|
|
41
|
-
"@crombolo/render": "0.
|
|
42
|
-
"@crombolo/schema": "0.
|
|
43
|
-
"@crombolo/transfer": "0.
|
|
33
|
+
"@crombolo/api-delivery": "0.18.0",
|
|
34
|
+
"@crombolo/api-management": "0.18.0",
|
|
35
|
+
"@crombolo/assistant": "0.18.0",
|
|
36
|
+
"@crombolo/auth": "0.18.0",
|
|
37
|
+
"@crombolo/backoffice-host": "0.18.0",
|
|
38
|
+
"@crombolo/contracts": "0.18.0",
|
|
39
|
+
"@crombolo/core": "0.18.0",
|
|
40
|
+
"@crombolo/data": "0.18.0",
|
|
41
|
+
"@crombolo/render": "0.18.0",
|
|
42
|
+
"@crombolo/schema": "0.18.0",
|
|
43
|
+
"@crombolo/transfer": "0.18.0",
|
|
44
44
|
"@logtape/logtape": "^2.3.8"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -75,6 +75,8 @@ export function createDocumentPort(
|
|
|
75
75
|
tracker: RedirectTracker = NO_REDIRECT_TRACKING,
|
|
76
76
|
/** The site's own directory: where `domains.toml` is, when the site keeps one. */
|
|
77
77
|
siteDir?: string,
|
|
78
|
+
/** Whether the site can send the e-mails a subscription asks for (story 045). */
|
|
79
|
+
notificationsAvailable = true,
|
|
78
80
|
): DocumentPort {
|
|
79
81
|
const repo = new DocumentRepository(db, options)
|
|
80
82
|
const domains = new DomainRepository(db)
|
|
@@ -415,9 +417,14 @@ export function createDocumentPort(
|
|
|
415
417
|
async setNotifications(key, principal, actionIds) {
|
|
416
418
|
const node = await repo.nodes.byKey(key)
|
|
417
419
|
const userId = await userIdOf(principal)
|
|
418
|
-
if (!node || node.objectType !== repo.objectType || userId === undefined) return
|
|
420
|
+
if (!node || node.objectType !== repo.objectType || userId === undefined) return 'notFound'
|
|
421
|
+
// Clearing is always allowed; asking for something the site cannot send is not.
|
|
422
|
+
if (!notificationsAvailable) {
|
|
423
|
+
const had = new Set(await notifications.subscribed(userId, node.id))
|
|
424
|
+
if (actionIds.some((id) => !had.has(id))) return 'unavailable'
|
|
425
|
+
}
|
|
419
426
|
await notifications.set(userId, node.id, actionIds)
|
|
420
|
-
return
|
|
427
|
+
return 'saved'
|
|
421
428
|
},
|
|
422
429
|
|
|
423
430
|
versions: (key) => repo.versions(key),
|
package/src/adapters/users.ts
CHANGED
|
@@ -119,6 +119,8 @@ export interface UserPortOptions {
|
|
|
119
119
|
nodes: NodeLookup
|
|
120
120
|
/** Signs a user out everywhere, as a password change set by someone else does. */
|
|
121
121
|
endSessions: (userId: number) => Promise<void>
|
|
122
|
+
/** A group save changed what it may do on these pages (story 045: the "rights" notification). */
|
|
123
|
+
onPagePermissionsChanged?: (principal: Principal, nodeKeys: string[]) => void | Promise<void>
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export function createUserPorts(
|
|
@@ -661,7 +663,11 @@ export function createUserPorts(
|
|
|
661
663
|
|
|
662
664
|
return {
|
|
663
665
|
users: userPort,
|
|
664
|
-
groups: createUserGroupPort(db, {
|
|
666
|
+
groups: createUserGroupPort(db, {
|
|
667
|
+
adminKey,
|
|
668
|
+
nodes,
|
|
669
|
+
onPagePermissionsChanged: options.onPagePermissionsChanged,
|
|
670
|
+
}),
|
|
665
671
|
userData: createUserDataPort(db),
|
|
666
672
|
}
|
|
667
673
|
}
|
|
@@ -763,7 +769,11 @@ function fromPermissions(permissions: UserGroupInput['permissions']): GranularPe
|
|
|
763
769
|
|
|
764
770
|
function createUserGroupPort(
|
|
765
771
|
db: Db,
|
|
766
|
-
deps: {
|
|
772
|
+
deps: {
|
|
773
|
+
adminKey: () => Promise<string | undefined>
|
|
774
|
+
nodes: NodeLookup
|
|
775
|
+
onPagePermissionsChanged?: UserPortOptions['onPagePermissionsChanged']
|
|
776
|
+
},
|
|
767
777
|
): UserGroupPort {
|
|
768
778
|
const groups = new UserGroupRepository(db)
|
|
769
779
|
const users = new UserRepository(db)
|
|
@@ -827,6 +837,34 @@ function createUserGroupPort(
|
|
|
827
837
|
return 'Success'
|
|
828
838
|
}
|
|
829
839
|
|
|
840
|
+
/** Each page's verbs for the group, by page key. */
|
|
841
|
+
const pageVerbs = (group: { granular: GranularPermission[] } | undefined) => {
|
|
842
|
+
const out = new Map<string, Set<string>>()
|
|
843
|
+
for (const row of group?.granular ?? [])
|
|
844
|
+
if (row.context === 'Document' && row.key) {
|
|
845
|
+
const verbs = out.get(row.key) ?? new Set<string>()
|
|
846
|
+
verbs.add(row.permission)
|
|
847
|
+
out.set(row.key, verbs)
|
|
848
|
+
}
|
|
849
|
+
return out
|
|
850
|
+
}
|
|
851
|
+
/** The pages whose verbs for the group a save changed. */
|
|
852
|
+
const changedPages = (before: Map<string, Set<string>>, after: Map<string, Set<string>>) =>
|
|
853
|
+
[...new Set([...before.keys(), ...after.keys()])].filter((key) => {
|
|
854
|
+
const a = before.get(key) ?? new Set<string>()
|
|
855
|
+
const b = after.get(key) ?? new Set<string>()
|
|
856
|
+
return a.size !== b.size || [...a].some((verb) => !b.has(verb))
|
|
857
|
+
})
|
|
858
|
+
const announcePermissions = async (
|
|
859
|
+
principal: Principal,
|
|
860
|
+
before: Map<string, Set<string>>,
|
|
861
|
+
key: string,
|
|
862
|
+
) => {
|
|
863
|
+
if (!deps.onPagePermissionsChanged) return
|
|
864
|
+
const changed = changedPages(before, pageVerbs(await groups.byKey(key)))
|
|
865
|
+
if (changed.length > 0) await deps.onPagePermissionsChanged(principal, changed)
|
|
866
|
+
}
|
|
867
|
+
|
|
830
868
|
const save = async (input: UserGroupInput, key: string): Promise<UserGroupStatus> => {
|
|
831
869
|
const result = await groups.save({
|
|
832
870
|
key,
|
|
@@ -906,6 +944,7 @@ function createUserGroupPort(
|
|
|
906
944
|
if (await groups.byKey(key)) return { status: 'DuplicateAlias' }
|
|
907
945
|
const saved = await save(input, key)
|
|
908
946
|
if (saved !== 'Success') return { status: saved }
|
|
947
|
+
await announcePermissions(principal, new Map(), key)
|
|
909
948
|
// A non-admin who creates a group joins it, as in Umbraco
|
|
910
949
|
if (!principal.isAdmin) await users.addToGroup(key, [principal.id])
|
|
911
950
|
return { status: 'Success', key }
|
|
@@ -917,7 +956,10 @@ function createUserGroupPort(
|
|
|
917
956
|
if (!isMember(principal, group.key)) return 'Forbidden'
|
|
918
957
|
const valid = await validate(principal, input, group)
|
|
919
958
|
if (valid !== 'Success') return valid
|
|
920
|
-
|
|
959
|
+
const before = pageVerbs(group)
|
|
960
|
+
const saved = await save(input, group.key)
|
|
961
|
+
if (saved === 'Success') await announcePermissions(principal, before, group.key)
|
|
962
|
+
return saved
|
|
921
963
|
},
|
|
922
964
|
|
|
923
965
|
async delete(principal, keys) {
|
package/src/approvals.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* This decides: who may act, what an edit does to a waiting request, when a
|
|
8
8
|
* stage is done, and when the page is published.
|
|
9
9
|
*/
|
|
10
|
+
|
|
10
11
|
import type { Principal } from '@crombolo/api-management'
|
|
11
12
|
import {
|
|
12
13
|
approvalsNeeded,
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
type StoredWorkflow,
|
|
37
38
|
UserGroupRepository,
|
|
38
39
|
} from '@crombolo/data'
|
|
40
|
+
import type { ContentNotifier } from './content-notifications.ts'
|
|
39
41
|
import type { EmailPort } from './email.ts'
|
|
40
42
|
|
|
41
43
|
/** What publishing and unpublishing go through when approval says yes. */
|
|
@@ -57,6 +59,8 @@ export interface ApprovalsOptions {
|
|
|
57
59
|
email?: EmailPort
|
|
58
60
|
/** Where a link in an e-mail points, e.g. `https://site.example/crombolo`. */
|
|
59
61
|
backOfficeUrl?: string
|
|
62
|
+
/** Queues the content notifications a publish on approval makes (story 045). */
|
|
63
|
+
notifier?: ContentNotifier
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/** A refusal a person reads: why, and what to do instead. */
|
|
@@ -426,6 +430,12 @@ export function createApprovals(options: ApprovalsOptions): Approvals {
|
|
|
426
430
|
request.action === 'publish' ? 'Published on approval' : 'Unpublished on approval',
|
|
427
431
|
`through "${request.workflow.name}"`,
|
|
428
432
|
)
|
|
433
|
+
if (request.action === 'publish')
|
|
434
|
+
await options.notifier?.raise(
|
|
435
|
+
{ actions: ['Umb.Document.Publish'], key: request.nodeKey, keyKind: 'document' },
|
|
436
|
+
principal ? { key: principal.id, name: principal.name } : null,
|
|
437
|
+
`approved through "${request.workflow.name}"`,
|
|
438
|
+
)
|
|
429
439
|
await notify(
|
|
430
440
|
'editor',
|
|
431
441
|
request,
|
package/src/components.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { join, relative, resolve, sep } from 'node:path'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -196,6 +196,82 @@ export function componentFileFor(root: string, alias: string): string {
|
|
|
196
196
|
return join(root, `${alias}.tsx`)
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/** What an e-mail template looks like: a default export over `EmailProps`. */
|
|
200
|
+
export function emailScaffold(alias: string): string {
|
|
201
|
+
const name = componentName(alias)
|
|
202
|
+
return `import type { EmailProps } from '@crombolo/crombolo'
|
|
203
|
+
|
|
204
|
+
export default function ${name}({ form, fields, site }: EmailProps) {
|
|
205
|
+
return (
|
|
206
|
+
<html lang="en">
|
|
207
|
+
<body>
|
|
208
|
+
<h1>{form.name}</h1>
|
|
209
|
+
<table>
|
|
210
|
+
{fields.map((field) => (
|
|
211
|
+
<tr>
|
|
212
|
+
<th>{field.caption}</th>
|
|
213
|
+
<td>
|
|
214
|
+
{field.leftOut
|
|
215
|
+
? 'Not included'
|
|
216
|
+
: field.answered
|
|
217
|
+
? field.values.join(', ')
|
|
218
|
+
: 'Not answered'}
|
|
219
|
+
</td>
|
|
220
|
+
</tr>
|
|
221
|
+
))}
|
|
222
|
+
</table>
|
|
223
|
+
<p>
|
|
224
|
+
Sent from <a href={site.url}>{site.name}</a>
|
|
225
|
+
</p>
|
|
226
|
+
</body>
|
|
227
|
+
</html>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The folder e-mail templates live in, as an alias prefix. */
|
|
234
|
+
export const EMAIL_FOLDER = 'emails/'
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The components the forms designer offers as e-mail designs: those in
|
|
238
|
+
* `components/emails/`, and any a form already names, so a picker never loses
|
|
239
|
+
* the one that is set. A layout there takes `EmailLayoutProps`, so it is not offered.
|
|
240
|
+
*/
|
|
241
|
+
export function listEmailTemplates(
|
|
242
|
+
root: string,
|
|
243
|
+
named: Iterable<string> = [],
|
|
244
|
+
): { alias: string; name: string }[] {
|
|
245
|
+
const components = listComponents(root)
|
|
246
|
+
const found = new Set<string>()
|
|
247
|
+
for (const component of components)
|
|
248
|
+
if (
|
|
249
|
+
component.alias.startsWith(EMAIL_FOLDER) &&
|
|
250
|
+
!/\bEmailLayoutProps\b/.test(readFileSync(component.path, 'utf8'))
|
|
251
|
+
)
|
|
252
|
+
found.add(component.alias)
|
|
253
|
+
const present = new Set(components.map((component) => component.alias))
|
|
254
|
+
for (const alias of named) if (present.has(alias)) found.add(alias)
|
|
255
|
+
const items = [...found].sort().map((alias) => ({ alias, name: readableName(alias) }))
|
|
256
|
+
// Two folders with the same file name would read alike, so those keep their path.
|
|
257
|
+
const counts = new Map<string, number>()
|
|
258
|
+
for (const item of items) counts.set(item.name, (counts.get(item.name) ?? 0) + 1)
|
|
259
|
+
return items.map((item) =>
|
|
260
|
+
(counts.get(item.name) ?? 0) > 1 ? { ...item, name: `${item.name} (${item.alias})` } : item,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** `emails/contactReceipt` → "Contact receipt". */
|
|
265
|
+
function readableName(alias: string): string {
|
|
266
|
+
const base = alias.split('/').pop() ?? alias
|
|
267
|
+
const words = base
|
|
268
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
269
|
+
.replace(/[-_]+/g, ' ')
|
|
270
|
+
.trim()
|
|
271
|
+
.toLowerCase()
|
|
272
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
273
|
+
}
|
|
274
|
+
|
|
199
275
|
/** What a component looks like: a function, taking whatever imports it hands it. */
|
|
200
276
|
export function componentScaffold(alias: string): string {
|
|
201
277
|
const name = componentName(alias)
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { GitConfig } from './git-routes.ts'
|
|
|
14
14
|
import { checkIdentityProviders, type IdentityProvider } from './identity-providers.ts'
|
|
15
15
|
import type { MediaStore } from './media-store.ts'
|
|
16
16
|
import type { SchemaStore } from './schema-store.ts'
|
|
17
|
+
import { setting } from './settings.ts'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Runtime settings for a site. Umbraco's equivalents live in appsettings.json;
|
|
@@ -497,11 +498,11 @@ export function loadConfig(
|
|
|
497
498
|
cwd = process.cwd(),
|
|
498
499
|
): CromboloConfig {
|
|
499
500
|
const fromEnv: CromboloConfig = {
|
|
500
|
-
port: Number(
|
|
501
|
-
maxRequestBodyBytes:
|
|
502
|
-
? Number(
|
|
501
|
+
port: Number(setting('PORT') ?? DEFAULTS.port),
|
|
502
|
+
maxRequestBodyBytes: setting('CROMBOLO_MAX_REQUEST_BODY_MB')
|
|
503
|
+
? Number(setting('CROMBOLO_MAX_REQUEST_BODY_MB')) * 1024 * 1024
|
|
503
504
|
: DEFAULTS.maxRequestBodyBytes,
|
|
504
|
-
backOfficePath:
|
|
505
|
+
backOfficePath: setting('CROMBOLO_BACKOFFICE_PATH') ?? DEFAULTS.backOfficePath,
|
|
505
506
|
defaultUiLanguage: DEFAULTS.defaultUiLanguage,
|
|
506
507
|
keepUserLoggedIn: DEFAULTS.keepUserLoggedIn,
|
|
507
508
|
allowLocalLogin: DEFAULTS.allowLocalLogin,
|
|
@@ -509,135 +510,135 @@ export function loadConfig(
|
|
|
509
510
|
versionCheckPeriod: DEFAULTS.versionCheckPeriod,
|
|
510
511
|
umbracoCssPath: DEFAULTS.umbracoCssPath,
|
|
511
512
|
signalRSkipNegotiation: DEFAULTS.signalRSkipNegotiation,
|
|
512
|
-
development:
|
|
513
|
-
sqliteFile:
|
|
514
|
-
adminLogin:
|
|
515
|
-
adminPassword:
|
|
513
|
+
development: setting('NODE_ENV') !== 'production',
|
|
514
|
+
sqliteFile: setting('CROMBOLO_SQLITE_FILE') ?? DEFAULTS.sqliteFile,
|
|
515
|
+
adminLogin: setting('CROMBOLO_ADMIN_LOGIN') ?? DEFAULTS.adminLogin,
|
|
516
|
+
adminPassword: setting('CROMBOLO_ADMIN_PASSWORD'),
|
|
516
517
|
apiClient:
|
|
517
|
-
|
|
518
|
+
setting('CROMBOLO_API_CLIENT_ID') || setting('CROMBOLO_API_CLIENT_SECRET')
|
|
518
519
|
? {
|
|
519
|
-
id:
|
|
520
|
-
secret:
|
|
520
|
+
id: setting('CROMBOLO_API_CLIENT_ID') ?? '',
|
|
521
|
+
secret: setting('CROMBOLO_API_CLIENT_SECRET') ?? '',
|
|
521
522
|
}
|
|
522
523
|
: undefined,
|
|
523
|
-
localAdmin:
|
|
524
|
-
setupCode:
|
|
525
|
-
secretKey:
|
|
526
|
-
secureCookies:
|
|
527
|
-
siteName:
|
|
524
|
+
localAdmin: setting('CROMBOLO_LOCAL_ADMIN') === 'true',
|
|
525
|
+
setupCode: setting('CROMBOLO_SETUP_CODE'),
|
|
526
|
+
secretKey: setting('CROMBOLO_SECRET_KEY'),
|
|
527
|
+
secureCookies: setting('CROMBOLO_INSECURE_COOKIES') !== 'true',
|
|
528
|
+
siteName: setting('CROMBOLO_SITE_NAME') ?? DEFAULTS.siteName,
|
|
528
529
|
siteDir: cwd,
|
|
529
|
-
componentsDir:
|
|
530
|
-
componentsCacheDir:
|
|
530
|
+
componentsDir: setting('CROMBOLO_COMPONENTS_DIR') ?? DEFAULTS.componentsDir,
|
|
531
|
+
componentsCacheDir: setting('CROMBOLO_COMPONENTS_CACHE_DIR') ?? DEFAULTS.componentsCacheDir,
|
|
531
532
|
componentsSnapshot: {
|
|
532
|
-
...(
|
|
533
|
-
? { gateTtlMs: Number(
|
|
533
|
+
...(setting('CROMBOLO_COMPONENTS_GATE_MS')
|
|
534
|
+
? { gateTtlMs: Number(setting('CROMBOLO_COMPONENTS_GATE_MS')) }
|
|
534
535
|
: {}),
|
|
535
|
-
...(
|
|
536
|
-
? { maxGenerations: Number(
|
|
536
|
+
...(setting('CROMBOLO_COMPONENTS_GENERATION_LIMIT')
|
|
537
|
+
? { maxGenerations: Number(setting('CROMBOLO_COMPONENTS_GENERATION_LIMIT')) }
|
|
537
538
|
: {}),
|
|
538
|
-
...(
|
|
539
|
-
? { keep: Number(
|
|
539
|
+
...(setting('CROMBOLO_COMPONENTS_KEEP')
|
|
540
|
+
? { keep: Number(setting('CROMBOLO_COMPONENTS_KEEP')) }
|
|
540
541
|
: {}),
|
|
541
|
-
...(
|
|
542
|
-
? { minSwapIntervalMs: Number(
|
|
542
|
+
...(setting('CROMBOLO_COMPONENTS_SWAP_MS')
|
|
543
|
+
? { minSwapIntervalMs: Number(setting('CROMBOLO_COMPONENTS_SWAP_MS')) }
|
|
543
544
|
: {}),
|
|
544
545
|
},
|
|
545
|
-
renderTimeoutMs: Number(
|
|
546
|
-
schemaDir:
|
|
547
|
-
schemaRevision:
|
|
548
|
-
nodeId:
|
|
549
|
-
role: roleFromEnvironment(
|
|
546
|
+
renderTimeoutMs: Number(setting('CROMBOLO_RENDER_TIMEOUT_MS') ?? DEFAULTS.renderTimeoutMs),
|
|
547
|
+
schemaDir: setting('CROMBOLO_SCHEMA_DIR') ?? DEFAULTS.schemaDir,
|
|
548
|
+
schemaRevision: setting('CROMBOLO_SCHEMA_REVISION') ?? DEFAULTS.schemaRevision,
|
|
549
|
+
nodeId: setting('CROMBOLO_NODE_ID') ?? `${hostname()}:${process.pid}`,
|
|
550
|
+
role: roleFromEnvironment(setting('CROMBOLO_ROLE')),
|
|
550
551
|
git: undefined,
|
|
551
552
|
schemaStore: undefined,
|
|
552
|
-
schemaCacheDir:
|
|
553
|
+
schemaCacheDir: setting('CROMBOLO_SCHEMA_CACHE_DIR') ?? join(tmpdir(), 'crombolo-schema'),
|
|
553
554
|
// Resolved again below, once it is known whether a store was configured.
|
|
554
555
|
schemaWritable:
|
|
555
|
-
|
|
556
|
-
?
|
|
557
|
-
:
|
|
558
|
-
syncSchemaAtBoot:
|
|
559
|
-
dialect:
|
|
560
|
-
pgDump:
|
|
556
|
+
setting('CROMBOLO_SCHEMA_WRITABLE') !== undefined
|
|
557
|
+
? setting('CROMBOLO_SCHEMA_WRITABLE') === 'true'
|
|
558
|
+
: setting('NODE_ENV') !== 'production',
|
|
559
|
+
syncSchemaAtBoot: setting('CROMBOLO_SYNC_SCHEMA_AT_BOOT') !== 'false',
|
|
560
|
+
dialect: setting('CROMBOLO_DB') === 'postgres' ? 'postgres' : 'sqlite',
|
|
561
|
+
pgDump: setting('CROMBOLO_PG_DUMP'),
|
|
561
562
|
upgradePolicy:
|
|
562
|
-
|
|
563
|
-
(
|
|
563
|
+
setting('CROMBOLO_UPGRADE_POLICY') === 'pending-allowed' ||
|
|
564
|
+
(setting('CROMBOLO_UPGRADE_POLICY') === undefined && setting('NODE_ENV') !== 'production')
|
|
564
565
|
? 'pending-allowed'
|
|
565
566
|
: 'strict',
|
|
566
567
|
versionCleanup: {
|
|
567
|
-
enabled:
|
|
568
|
+
enabled: setting('CROMBOLO_VERSION_CLEANUP') === 'true',
|
|
568
569
|
keepAllVersionsNewerThanDays: Number(
|
|
569
|
-
|
|
570
|
+
setting('CROMBOLO_VERSION_CLEANUP_KEEP_ALL_DAYS') ?? DEFAULTS.keepAllVersionsNewerThanDays,
|
|
570
571
|
),
|
|
571
572
|
keepLatestVersionPerDayForDays: Number(
|
|
572
|
-
|
|
573
|
+
setting('CROMBOLO_VERSION_CLEANUP_KEEP_LATEST_DAYS') ??
|
|
573
574
|
DEFAULTS.keepLatestVersionPerDayForDays,
|
|
574
575
|
),
|
|
575
576
|
},
|
|
576
577
|
auditRetentionDays: nonNegative(
|
|
577
|
-
|
|
578
|
+
setting('CROMBOLO_AUDIT_RETENTION_DAYS'),
|
|
578
579
|
DEFAULTS.auditRetentionDays,
|
|
579
580
|
),
|
|
580
|
-
mediaDir:
|
|
581
|
+
mediaDir: setting('CROMBOLO_MEDIA_DIR') ?? DEFAULTS.mediaDir,
|
|
581
582
|
mediaStore: undefined,
|
|
582
|
-
logsDir:
|
|
583
|
-
logLevel: parseLogLevel(
|
|
584
|
-
logToConsole:
|
|
583
|
+
logsDir: setting('CROMBOLO_LOGS_DIR') ?? DEFAULTS.logsDir,
|
|
584
|
+
logLevel: parseLogLevel(setting('CROMBOLO_LOG_LEVEL')),
|
|
585
|
+
logToConsole: setting('CROMBOLO_LOG_TO_CONSOLE') !== 'false',
|
|
585
586
|
diagnostics: {
|
|
586
|
-
traceRequests:
|
|
587
|
-
heapIntervalSeconds: Math.max(0, Number(
|
|
587
|
+
traceRequests: setting('CROMBOLO_TRACE_REQUESTS') === 'true',
|
|
588
|
+
heapIntervalSeconds: Math.max(0, Number(setting('CROMBOLO_HEAP_INTERVAL_SECONDS') ?? 0) || 0),
|
|
588
589
|
},
|
|
589
|
-
stylesheetsDir:
|
|
590
|
-
scriptsDir:
|
|
591
|
-
usernameIsEmail:
|
|
592
|
-
applicationUrl:
|
|
590
|
+
stylesheetsDir: setting('CROMBOLO_CSS_DIR') ?? DEFAULTS.stylesheetsDir,
|
|
591
|
+
scriptsDir: setting('CROMBOLO_SCRIPTS_DIR') ?? DEFAULTS.scriptsDir,
|
|
592
|
+
usernameIsEmail: setting('CROMBOLO_USERNAME_IS_EMAIL') !== 'false',
|
|
593
|
+
applicationUrl: setting('CROMBOLO_APPLICATION_URL') ?? '',
|
|
593
594
|
sendUserLink: undefined,
|
|
594
595
|
email: undefined,
|
|
595
|
-
allowMemberRegistration:
|
|
596
|
+
allowMemberRegistration: setting('CROMBOLO_ALLOW_MEMBER_REGISTRATION') === 'true',
|
|
596
597
|
memberRegistrationType:
|
|
597
|
-
|
|
598
|
-
memberRegistrationGroups: (
|
|
598
|
+
setting('CROMBOLO_MEMBER_REGISTRATION_TYPE') ?? DEFAULTS.memberRegistrationType,
|
|
599
|
+
memberRegistrationGroups: (setting('CROMBOLO_MEMBER_REGISTRATION_GROUPS') ?? '')
|
|
599
600
|
.split(',')
|
|
600
601
|
.map((name) => name.trim())
|
|
601
602
|
.filter(Boolean),
|
|
602
603
|
memberSessionMinutes: Number(
|
|
603
|
-
|
|
604
|
+
setting('CROMBOLO_MEMBER_SESSION_MINUTES') ?? DEFAULTS.memberSessionMinutes,
|
|
604
605
|
),
|
|
605
606
|
maxFailedPasswordAttempts: Number(
|
|
606
|
-
|
|
607
|
+
setting('CROMBOLO_MAX_FAILED_PASSWORD_ATTEMPTS') ?? DEFAULTS.maxFailedPasswordAttempts,
|
|
607
608
|
),
|
|
608
609
|
// No environment variable: a provider is a preset, which is code.
|
|
609
610
|
memberIdentityProviders: [],
|
|
610
611
|
backofficeIdentityProviders: [],
|
|
611
612
|
remote: {
|
|
612
|
-
enabled:
|
|
613
|
-
environment:
|
|
614
|
-
protected:
|
|
615
|
-
approvals:
|
|
616
|
-
maxUploadBytes: Number(
|
|
613
|
+
enabled: setting('CROMBOLO_REMOTE_ENABLED') === 'true',
|
|
614
|
+
environment: setting('CROMBOLO_REMOTE_ENVIRONMENT') || 'production',
|
|
615
|
+
protected: setting('CROMBOLO_REMOTE_PROTECTED') !== 'false',
|
|
616
|
+
approvals: setting('CROMBOLO_REMOTE_APPROVALS') === 'true',
|
|
617
|
+
maxUploadBytes: Number(setting('CROMBOLO_REMOTE_MAX_UPLOAD_GB') ?? 50) * 1024 ** 3,
|
|
617
618
|
},
|
|
618
619
|
redirects: [],
|
|
619
|
-
trackRedirects:
|
|
620
|
-
applyBundleContentOnInstall:
|
|
620
|
+
trackRedirects: setting('CROMBOLO_TRACK_REDIRECTS') !== 'false',
|
|
621
|
+
applyBundleContentOnInstall: setting('CROMBOLO_APPLY_BUNDLE_CONTENT') !== 'false',
|
|
621
622
|
// Resolved again below, once it is known whether a store was configured.
|
|
622
|
-
runtimeInstalls:
|
|
623
|
+
runtimeInstalls: setting('CROMBOLO_RUNTIME_INSTALLS') !== 'false',
|
|
623
624
|
seo: stripUndefined({
|
|
624
|
-
sitemap: envFlag(
|
|
625
|
-
indexable: envFlag(
|
|
625
|
+
sitemap: envFlag(setting('CROMBOLO_SITEMAP')),
|
|
626
|
+
indexable: envFlag(setting('CROMBOLO_INDEXABLE')),
|
|
626
627
|
}),
|
|
627
628
|
deliveryApi: stripUndefined({
|
|
628
|
-
enabled: envFlag(
|
|
629
|
-
apiKey:
|
|
630
|
-
publicAccess: envFlag(
|
|
629
|
+
enabled: envFlag(setting('CROMBOLO_DELIVERY_API')),
|
|
630
|
+
apiKey: setting('CROMBOLO_DELIVERY_API_KEY') || undefined,
|
|
631
|
+
publicAccess: envFlag(setting('CROMBOLO_DELIVERY_API_PUBLIC')),
|
|
631
632
|
media:
|
|
632
|
-
envFlag(
|
|
633
|
+
envFlag(setting('CROMBOLO_DELIVERY_API_MEDIA')) === undefined
|
|
633
634
|
? undefined
|
|
634
|
-
: { enabled: envFlag(
|
|
635
|
+
: { enabled: envFlag(setting('CROMBOLO_DELIVERY_API_MEDIA')) },
|
|
635
636
|
}),
|
|
636
637
|
// No environment variable: a bundle is an import, which is code.
|
|
637
638
|
bundles: [],
|
|
638
|
-
marketplaceUrl:
|
|
639
|
-
packageKeyword:
|
|
640
|
-
npmRegistry:
|
|
639
|
+
marketplaceUrl: setting('CROMBOLO_MARKETPLACE_URL') ?? DEFAULTS.marketplaceUrl,
|
|
640
|
+
packageKeyword: setting('CROMBOLO_PACKAGE_KEYWORD') ?? DEFAULTS.packageKeyword,
|
|
641
|
+
npmRegistry: setting('CROMBOLO_NPM_REGISTRY') ?? DEFAULTS.npmRegistry,
|
|
641
642
|
// No environment variable: enabling it takes a provider, which is code.
|
|
642
643
|
assistant: undefined,
|
|
643
644
|
}
|
|
@@ -661,7 +662,7 @@ export function loadConfig(
|
|
|
661
662
|
// it — that is the point of configuring one. An explicit setting still wins.
|
|
662
663
|
if (
|
|
663
664
|
merged.schemaStore &&
|
|
664
|
-
|
|
665
|
+
setting('CROMBOLO_SCHEMA_WRITABLE') === undefined &&
|
|
665
666
|
overrides.schemaWritable === undefined
|
|
666
667
|
) {
|
|
667
668
|
merged.schemaWritable = true
|
|
@@ -670,7 +671,7 @@ export function loadConfig(
|
|
|
670
671
|
// them would not either. An explicit setting still wins.
|
|
671
672
|
if (
|
|
672
673
|
merged.schemaStore &&
|
|
673
|
-
|
|
674
|
+
setting('CROMBOLO_RUNTIME_INSTALLS') === undefined &&
|
|
674
675
|
overrides.runtimeInstalls === undefined
|
|
675
676
|
) {
|
|
676
677
|
merged.runtimeInstalls = false
|
|
@@ -727,7 +728,7 @@ function stripUndefined<T extends object>(value: T): Partial<T> {
|
|
|
727
728
|
*/
|
|
728
729
|
export function describeDatabase(
|
|
729
730
|
config: Pick<CromboloConfig, 'dialect' | 'sqliteFile'>,
|
|
730
|
-
url: string | undefined =
|
|
731
|
+
url: string | undefined = setting('CROMBOLO_POSTGRES_URL') ?? setting('DATABASE_URL'),
|
|
731
732
|
): string {
|
|
732
733
|
if (config.dialect !== 'postgres') return config.sqliteFile
|
|
733
734
|
if (!url) return 'postgres'
|
|
@@ -739,4 +740,4 @@ export function describeDatabase(
|
|
|
739
740
|
}
|
|
740
741
|
}
|
|
741
742
|
|
|
742
|
-
export const VERSION = '0.
|
|
743
|
+
export const VERSION = '0.18.0'
|