@crombolo/cli 0.9.1 → 0.11.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/bin/crombolo.ts +161 -37
- package/package.json +7 -7
- package/src/operations/commands.ts +244 -333
- package/src/templates.ts +1 -1
- package/templates/basic/bundle/package.json +16 -0
- package/templates/demo/harbourstone/bundle/package.json +16 -0
- package/templates/demo/harbourstone/files/schema/forms/contact.toml +2 -2
- package/templates/starter/bundle/package.json +16 -0
- package/src/operations/sections.ts +0 -423
- /package/templates/basic/bundle/{bundle.json → content/bundle.json} +0 -0
- /package/templates/basic/bundle/{nodes → content/nodes}/36955f6e-d110-469e-b280-72096eccd148.json +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/180cc8ef/distillery-from-the-cliff-path.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/68c9cfdf/bottle-brine-and-kiln.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/6be45535/bottle-oloroso-pier.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/6d8d6398/bottle-the-cove.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/7b4713f8/copper-pot-stills.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/c114b7d8/bottle-neap-tide.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/fd337567/casks-in-the-sea-facing-warehouse.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{bundle.json → content/bundle.json} +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/069cc3c5-c9f5-4792-90c9-e2bfcc4855b6.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/13788fe7-1a45-4b48-8ae4-f4227e154e1e.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/180cc8ef-caf1-4316-b6a7-e3b5aee41ffd.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/22144db2-e214-4006-a0bb-53b5bd121fd6.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/26e87610-e2d6-4b47-bf92-3967f7186907.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/2caa099b-3884-472d-9cea-797a0f0c376f.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/2efbdb5b-7888-4a49-bca5-c0f85b76e472.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/2fe03891-1294-4344-8e24-47e8b212e5b0.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/38b009f7-03c0-4fda-b5c7-843ad6044e9c.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/3d312cc8-2937-40e1-ba14-fd781c9a862d.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/3dc30fc1-973a-4217-b26f-24902210be17.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/40107d59-f3e9-4592-aee8-5b03d4586cbd.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/483a0d3b-a10c-4c9d-8c81-d9607d37e968.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/4b760c0b-b83a-4eee-aa29-c225cfabf19b.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/4e5c7f3f-54cb-4328-ad61-bc4c2736b890.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/62f479ff-0875-453e-99ae-3bf1bec5e3c8.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/68c9cfdf-6b40-47da-a1d2-17256a6cc6db.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/6ad8f0f3-9334-47f1-b805-80c4b4b4fa88.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/6be45535-83e5-4469-acf0-4127016721ae.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/6d8d6398-738f-4f38-ad3c-e5d6e9a971fe.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/7b4713f8-d917-457d-a9d8-7dd01a3617b4.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/88ec1185-53b5-439a-a2ce-3f56603be4ff.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/97469df2-107c-4010-8c14-4de88f735529.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/a2eb439f-71d9-4ea6-89d7-c5bc72618281.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/a562f0a2-cb6e-4ead-a1ce-4d180632419f.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/a5e36a18-a2ef-4eb5-85b4-7e12b21cca06.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/b1876d3a-50a7-4358-84bf-a0899932ffa9.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/b4e38bbd-c1a3-431a-b7a9-5868aa32993a.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/c114b7d8-0138-404b-9d8f-a39d992577a2.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/cb4c569e-c28a-43b6-a4d7-d33bb544e973.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/cd681b4b-9c4e-4624-9c13-450aa6a9c135.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/d60ef12b-0f0a-4db5-9763-21fa52b8d4da.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/d7f13222-d060-4496-b86d-bef21da963c1.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/f36dd4a9-927c-4ca3-a790-c9c859ac7302.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/fd337567-df67-41ca-ae94-9d84d8d6042f.json +0 -0
- /package/templates/starter/bundle/{bundle.json → content/bundle.json} +0 -0
- /package/templates/starter/bundle/{nodes → content/nodes}/634dd948-cf83-4cad-9b7b-7c938b33da76.json +0 -0
- /package/templates/starter/bundle/{nodes → content/nodes}/64d95359-d91a-4050-b440-3e5124a74999.json +0 -0
- /package/templates/starter/bundle/{nodes → content/nodes}/d2997ebb-36ca-47cb-bbc8-31fefca787c0.json +0 -0
|
@@ -5,17 +5,23 @@
|
|
|
5
5
|
* a platform's own tooling — gets the same results as data. One implementation,
|
|
6
6
|
* so the two can never disagree about what an import or an upgrade does.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { userInfo } from 'node:os'
|
|
9
|
+
import { ROOT_ACCESS } from '@crombolo/core'
|
|
9
10
|
import {
|
|
11
|
+
type AuditCategory,
|
|
12
|
+
bundleStateKey,
|
|
13
|
+
bundleTablesIn,
|
|
10
14
|
connect,
|
|
11
15
|
cromboloPlan,
|
|
12
16
|
type Db,
|
|
13
17
|
describeRefFailure,
|
|
14
18
|
type NodeRow,
|
|
19
|
+
type PlannedStep,
|
|
15
20
|
pauseWrites,
|
|
21
|
+
pendingMigrations,
|
|
16
22
|
planUpgrade,
|
|
23
|
+
purgeBundleTables,
|
|
17
24
|
readWritesPaused,
|
|
18
|
-
recordInLedger,
|
|
19
25
|
resolveNodeRef,
|
|
20
26
|
resumeWrites,
|
|
21
27
|
type WritesPaused,
|
|
@@ -24,66 +30,65 @@ import {
|
|
|
24
30
|
import { loadSchemaDirectory, loadValueMigrations, runFix, runUpgrade } from '@crombolo/schema'
|
|
25
31
|
import {
|
|
26
32
|
type BackupResult,
|
|
33
|
+
type BundleChain,
|
|
34
|
+
BundleConfigurationError,
|
|
27
35
|
backupBefore,
|
|
28
36
|
bootSchema,
|
|
29
37
|
bootstrapDatabase,
|
|
38
|
+
bundleChainsFor,
|
|
39
|
+
CONTENT_OBJECT_TYPES,
|
|
40
|
+
CommandError,
|
|
41
|
+
type ContentCheckResult,
|
|
42
|
+
type ContentImportInput,
|
|
43
|
+
type ContentImportResult,
|
|
30
44
|
type CromboloConfig,
|
|
45
|
+
checkBundleContent,
|
|
31
46
|
checkSchemaDirectory,
|
|
32
|
-
componentAliasesIn,
|
|
33
47
|
createFileIntake,
|
|
34
48
|
createMediaPort,
|
|
49
|
+
installBundleContent,
|
|
35
50
|
installDatabase,
|
|
36
51
|
MediaFileStore,
|
|
37
52
|
mediaStoreFor,
|
|
38
|
-
|
|
53
|
+
pendingBundleSteps,
|
|
54
|
+
planBundleUpgrades,
|
|
39
55
|
readBlobs,
|
|
56
|
+
readBundle,
|
|
57
|
+
recordAudit,
|
|
40
58
|
syncOptionsFor,
|
|
59
|
+
type TransferInput,
|
|
60
|
+
VERSION,
|
|
41
61
|
} from '@crombolo/server'
|
|
42
62
|
import {
|
|
43
|
-
|
|
44
|
-
type ContentSet,
|
|
45
|
-
checkBundle,
|
|
63
|
+
bundleIdFor,
|
|
46
64
|
exportBundle,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
loadResolutions,
|
|
50
|
-
type Resolution,
|
|
51
|
-
type TransferCheck,
|
|
65
|
+
isPackageName,
|
|
66
|
+
packageNameFor,
|
|
52
67
|
writeBundle,
|
|
68
|
+
writeBundlePackage,
|
|
53
69
|
} from '@crombolo/transfer'
|
|
54
|
-
import {
|
|
55
|
-
applySections,
|
|
56
|
-
type CarriedSections,
|
|
57
|
-
carriedSections,
|
|
58
|
-
carriesSections,
|
|
59
|
-
planSections,
|
|
60
|
-
promoteBackups,
|
|
61
|
-
replacementsNeedingPermission,
|
|
62
|
-
type SectionFile,
|
|
63
|
-
type SectionOptions,
|
|
64
|
-
type SectionPlan,
|
|
65
|
-
type SectionResult,
|
|
66
|
-
stageBackups,
|
|
67
|
-
} from './sections.ts'
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
type Resolution,
|
|
73
|
-
type TransferCheck,
|
|
74
|
-
} from '@crombolo/transfer'
|
|
75
|
-
// The section half of an install, exported for the same reason the commands
|
|
76
|
-
// are: tooling that applies a bundle without a terminal needs the plan too.
|
|
71
|
+
// The installer lives in the server, so the backoffice can run it too; these
|
|
72
|
+
// are exported for the same reason the commands are: tooling that applies a
|
|
73
|
+
// bundle without a terminal needs the plan and the refusals as well.
|
|
77
74
|
export {
|
|
78
75
|
applySections,
|
|
76
|
+
type BundleSummary,
|
|
79
77
|
type CarriedSections,
|
|
78
|
+
CONTENT_OBJECT_TYPES,
|
|
79
|
+
CommandError,
|
|
80
|
+
type ContentCheckResult,
|
|
81
|
+
type ContentImportInput,
|
|
82
|
+
type ContentImportResult,
|
|
80
83
|
carriedSections,
|
|
81
84
|
carriesSections,
|
|
82
85
|
type FileAction,
|
|
83
86
|
planSections,
|
|
84
87
|
promoteBackups,
|
|
88
|
+
type ResolvedBundle,
|
|
85
89
|
type RestoredFiles,
|
|
86
90
|
RUN_BACKUP_DIR,
|
|
91
|
+
readBundle,
|
|
87
92
|
replacementsNeedingPermission,
|
|
88
93
|
restoreSectionFiles,
|
|
89
94
|
type SectionFile,
|
|
@@ -91,299 +96,41 @@ export {
|
|
|
91
96
|
type SectionPlan,
|
|
92
97
|
type SectionResult,
|
|
93
98
|
stageBackups,
|
|
94
|
-
|
|
99
|
+
type TransferInput,
|
|
100
|
+
transferOptions,
|
|
101
|
+
} from '@crombolo/server'
|
|
102
|
+
export {
|
|
103
|
+
isResolution,
|
|
104
|
+
RESOLUTIONS,
|
|
105
|
+
type Resolution,
|
|
106
|
+
type TransferCheck,
|
|
107
|
+
} from '@crombolo/transfer'
|
|
95
108
|
export type { BackupResult, WritesPaused }
|
|
96
109
|
|
|
97
|
-
/**
|
|
98
|
-
export const CONTENT_OBJECT_TYPES = [ObjectTypes.Document, ObjectTypes.Media, ObjectTypes.Element]
|
|
99
|
-
|
|
100
|
-
/** A refusal the caller should read: nothing was written. */
|
|
101
|
-
export class CommandError extends Error {
|
|
102
|
-
constructor(
|
|
103
|
-
message: string,
|
|
104
|
-
readonly problems: string[] = [],
|
|
105
|
-
) {
|
|
106
|
-
super(message)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface BundleSummary {
|
|
111
|
-
id: string
|
|
112
|
-
from: string
|
|
113
|
-
schemaVersion: string
|
|
114
|
-
schemaRevision: string
|
|
115
|
-
snapshot: string
|
|
116
|
-
nodes: number
|
|
117
|
-
blobsCarried: number
|
|
118
|
-
blobBytes: number
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* The check an install that never reached the content reports.
|
|
123
|
-
*
|
|
124
|
-
* Nothing was examined, so nothing is outstanding: the refusal is in
|
|
125
|
-
* `problems`, and a caller reading `outstanding.length` must not be told there
|
|
126
|
-
* were content findings when there were none.
|
|
127
|
-
*/
|
|
128
|
-
const emptyCheck = (bundleId = ''): TransferCheck => ({
|
|
129
|
-
bundleId,
|
|
130
|
-
findings: [],
|
|
131
|
-
outstanding: [],
|
|
132
|
-
plan: [],
|
|
133
|
-
counts: { create: 0, update: 0, unchanged: 0, skip: 0 },
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
function summarise(set: ContentSet): BundleSummary {
|
|
137
|
-
const carried = set.manifest.blobs.filter((blob) => blob.included)
|
|
138
|
-
return {
|
|
139
|
-
id: set.manifest.id,
|
|
140
|
-
from: set.manifest.provenance.siteName || '',
|
|
141
|
-
schemaVersion: set.manifest.provenance.schemaVersion,
|
|
142
|
-
schemaRevision: set.manifest.provenance.schemaRevision,
|
|
143
|
-
snapshot: set.manifest.snapshot,
|
|
144
|
-
nodes: set.nodes.length,
|
|
145
|
-
blobsCarried: carried.length,
|
|
146
|
-
blobBytes: carried.reduce((total, blob) => total + (blob.size ?? 0), 0),
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Reads a bundle whole, or refuses: a half-copied bundle must never be checked as though complete. */
|
|
151
|
-
export function readBundle(dir: string): {
|
|
152
|
-
set: ContentSet
|
|
153
|
-
blobs: ReadonlyMap<string, string>
|
|
154
|
-
/** The sections it carries beyond its content; empty for a content-only bundle. */
|
|
155
|
-
carried: CarriedSections
|
|
156
|
-
} {
|
|
157
|
-
const loaded = loadBundle(dir)
|
|
158
|
-
const problems = loaded.problems.map((problem) => `${problem.file}: ${problem.message}`)
|
|
159
|
-
if (!loaded.set || problems.length > 0)
|
|
160
|
-
throw new CommandError(
|
|
161
|
-
'The bundle is incomplete or damaged; nothing was read from it.',
|
|
162
|
-
problems,
|
|
163
|
-
)
|
|
164
|
-
return { set: loaded.set, blobs: loaded.blobs, carried: carriedSections(loaded) }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export interface TransferInput extends SectionOptions {
|
|
168
|
-
/** The bundle's directory on this machine. */
|
|
169
|
-
dir: string
|
|
170
|
-
/** Where the bundle's roots go: a path or a key. */
|
|
171
|
-
under?: string
|
|
172
|
-
/** A decision per node key, on top of any the bundle carries. */
|
|
173
|
-
resolutions?: Record<string, Resolution>
|
|
174
|
-
resolveAll?: Resolution
|
|
175
|
-
allowMissingBlobs?: boolean
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** The options `check` and `import` share: saved decisions, then the caller's, then placement. */
|
|
179
|
-
export async function transferOptions(
|
|
180
|
-
db: Db,
|
|
181
|
-
config: CromboloConfig,
|
|
182
|
-
set: ContentSet,
|
|
183
|
-
input: TransferInput,
|
|
184
|
-
): Promise<{ options: CheckOptions; under: NodeRow | undefined; problems: string[] }> {
|
|
185
|
-
const saved = loadResolutions(input.dir, set.manifest.id)
|
|
186
|
-
const resolutions = { ...(saved.file?.byNode ?? {}), ...(input.resolutions ?? {}) }
|
|
187
|
-
const resolveAll = input.resolveAll ?? saved.file?.all ?? undefined
|
|
188
|
-
const underRef = input.under ?? saved.file?.under ?? undefined
|
|
189
|
-
let under: NodeRow | undefined
|
|
190
|
-
if (underRef) {
|
|
191
|
-
const resolved = await resolveNodeRef(db, CONTENT_OBJECT_TYPES, underRef)
|
|
192
|
-
if (!resolved.ok) throw new CommandError(describeRefFailure(underRef, resolved))
|
|
193
|
-
under = resolved.node
|
|
194
|
-
}
|
|
195
|
-
// Whether this environment has a media file is a question only its store can
|
|
196
|
-
// answer; without it `missing-blob` is a finding the check could never raise.
|
|
197
|
-
const store = await mediaStoreFor(config)
|
|
198
|
-
return {
|
|
199
|
-
options: {
|
|
200
|
-
under,
|
|
201
|
-
resolutions,
|
|
202
|
-
resolveAll,
|
|
203
|
-
componentAliases: componentAliasesIn(config.componentsDir),
|
|
204
|
-
allowMissingBlobs: input.allowMissingBlobs ?? false,
|
|
205
|
-
hasBlob: async (key: string) => Boolean(await store.get(key)),
|
|
206
|
-
backOfficePath: config.backOfficePath,
|
|
207
|
-
},
|
|
208
|
-
under,
|
|
209
|
-
problems: saved.problems,
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export interface ContentCheckResult {
|
|
214
|
-
bundle: BundleSummary
|
|
215
|
-
under: string | null
|
|
216
|
-
check: TransferCheck
|
|
217
|
-
/** What the carried sections would do here; undefined for a content-only bundle. */
|
|
218
|
-
sections: SectionPlan | undefined
|
|
219
|
-
/** Nothing outstanding: the bundle would import cleanly. */
|
|
220
|
-
clean: boolean
|
|
221
|
-
/** The decisions this check ran with — the bundle's saved ones under the caller's. */
|
|
222
|
-
decisions: { all: Resolution | null; byNode: Record<string, Resolution> }
|
|
223
|
-
problems: string[]
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** Read-only: what importing a bundle here would do. Its findings go to the Changes dashboard. */
|
|
110
|
+
/** Read-only: what installing a bundle here would do. Its findings go to the Changes dashboard. */
|
|
227
111
|
export async function contentCheck(
|
|
228
112
|
config: CromboloConfig,
|
|
229
113
|
input: TransferInput,
|
|
230
114
|
): Promise<ContentCheckResult> {
|
|
231
|
-
const { set, carried } = readBundle(input.dir)
|
|
232
115
|
const { db } = await bootstrapDatabase(config)
|
|
233
116
|
try {
|
|
234
|
-
|
|
235
|
-
const check = await checkBundle(db, set, prepared.options)
|
|
236
|
-
// Under this bundle's own scope, so two bundles in flight do not resolve each other's.
|
|
237
|
-
await writeReport(db, check.findings, { source: 'transfer', scope: set.manifest.id })
|
|
238
|
-
return {
|
|
239
|
-
bundle: summarise(set),
|
|
240
|
-
under: prepared.under ? (prepared.under.text ?? prepared.under.key) : null,
|
|
241
|
-
check,
|
|
242
|
-
sections: carriesSections(carried)
|
|
243
|
-
? await planSections(db, config, carried, input)
|
|
244
|
-
: undefined,
|
|
245
|
-
clean: check.outstanding.length === 0,
|
|
246
|
-
decisions: {
|
|
247
|
-
all: prepared.options.resolveAll ?? null,
|
|
248
|
-
byNode: prepared.options.resolutions ?? {},
|
|
249
|
-
},
|
|
250
|
-
problems: prepared.problems,
|
|
251
|
-
}
|
|
117
|
+
return await checkBundleContent(db, config, input)
|
|
252
118
|
} finally {
|
|
253
119
|
await db.close()
|
|
254
120
|
}
|
|
255
121
|
}
|
|
256
122
|
|
|
257
|
-
|
|
258
|
-
publish?: boolean
|
|
259
|
-
label?: string
|
|
260
|
-
/** Postgres: the caller has taken a backup itself. */
|
|
261
|
-
backupTaken?: boolean
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export interface ContentImportResult {
|
|
265
|
-
bundle: BundleSummary
|
|
266
|
-
backup: BackupResult
|
|
267
|
-
/**
|
|
268
|
-
* What the carried sections did, when the bundle carried any.
|
|
269
|
-
*
|
|
270
|
-
* Applied before the content and outside the content run's ledger entry: the
|
|
271
|
-
* files are now in `schema/` and the views directory, where the repository
|
|
272
|
-
* owns them — so a revert takes the content back and git takes the files.
|
|
273
|
-
*/
|
|
274
|
-
sections: SectionResult | undefined
|
|
275
|
-
/** Where the originals of any replaced files were kept, for a revert. */
|
|
276
|
-
replacedFilesKept?: string | undefined
|
|
277
|
-
/** Undefined when the import was refused; then nothing was written. */
|
|
278
|
-
runId: string | undefined
|
|
279
|
-
check: TransferCheck
|
|
280
|
-
blobs: { placed: number; bytes: number } | null
|
|
281
|
-
published: number
|
|
282
|
-
publishFailures: { key: string; reason: string }[]
|
|
283
|
-
problems: string[]
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Re-check, back up, then apply in one transaction and record the run. */
|
|
123
|
+
/** Back up, re-check, then apply in one transaction and record the run. */
|
|
287
124
|
export async function contentImport(
|
|
288
125
|
config: CromboloConfig,
|
|
289
126
|
input: ContentImportInput,
|
|
290
127
|
): Promise<ContentImportResult> {
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
const backup = await backupBefore(config, {
|
|
295
|
-
reason: 'content-import',
|
|
296
|
-
backupTaken: input.backupTaken,
|
|
297
|
-
})
|
|
128
|
+
// Read before anything is opened or backed up, so a bundle that is not one is
|
|
129
|
+
// refused before a backup is taken on its behalf.
|
|
130
|
+
readBundle(input.bundle, config.siteDir)
|
|
298
131
|
const { db } = await bootstrapDatabase(config)
|
|
299
132
|
try {
|
|
300
|
-
|
|
301
|
-
const started = performance.now()
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Structure first, but planned before anything is written.
|
|
305
|
-
*
|
|
306
|
-
* The order is forced: content naming a type this bundle brings cannot pass
|
|
307
|
-
* a check until that type exists, so the schema has to land before the
|
|
308
|
-
* content is checked. Which means a blocking schema change has to be caught
|
|
309
|
-
* *before* the first file is written — otherwise a refused install leaves a
|
|
310
|
-
* half-applied site behind. `planSections` answers that from an overlay
|
|
311
|
-
* copy, so the site is untouched until it is known the schema can apply.
|
|
312
|
-
*/
|
|
313
|
-
const plan = carriesSections(carried)
|
|
314
|
-
? await planSections(db, config, carried, input)
|
|
315
|
-
: undefined
|
|
316
|
-
const refuse = (problems: string[]): ContentImportResult => ({
|
|
317
|
-
bundle: summarise(set),
|
|
318
|
-
backup,
|
|
319
|
-
runId: undefined,
|
|
320
|
-
check: emptyCheck(set.manifest.id),
|
|
321
|
-
blobs: null,
|
|
322
|
-
published: 0,
|
|
323
|
-
publishFailures: [],
|
|
324
|
-
problems,
|
|
325
|
-
sections: undefined,
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
if (plan?.schema?.classification === 'breaking')
|
|
329
|
-
return refuse([
|
|
330
|
-
'the schema this bundle carries cannot be applied here without data work',
|
|
331
|
-
...plan.schema.findings,
|
|
332
|
-
])
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* A file this site already has is not replaced without being asked.
|
|
336
|
-
*
|
|
337
|
-
* Every section is editable in the backoffice — a stylesheet, a template, a
|
|
338
|
-
* content type — so an overwrite destroys somebody's work, and the site
|
|
339
|
-
* may have no git to recover it from. Refused by name, like any other
|
|
340
|
-
* finding a person has to answer.
|
|
341
|
-
*/
|
|
342
|
-
const needingPermission = plan ? replacementsNeedingPermission(plan, input) : []
|
|
343
|
-
if (needingPermission.length > 0)
|
|
344
|
-
return refuse([
|
|
345
|
-
`${needingPermission.length} file(s) here would be replaced; pass --replace-files to allow it`,
|
|
346
|
-
...needingPermission.map((file: SectionFile) => file.path),
|
|
347
|
-
])
|
|
348
|
-
|
|
349
|
-
// Staged whenever something *will* be replaced — which is decided by the
|
|
350
|
-
// plan, not by the permission check: by this point permission has been
|
|
351
|
-
// given, so the files needing it are exactly the ones to back up.
|
|
352
|
-
const willReplace = (plan?.files ?? []).some((file) => file.action === 'overwrite')
|
|
353
|
-
const staging = plan && willReplace ? stageBackups(config) : undefined
|
|
354
|
-
const sections = plan
|
|
355
|
-
? await applySections(db, config, carried, { ...input, backupDir: staging })
|
|
356
|
-
: undefined
|
|
357
|
-
const result = await importBundle(db, set, {
|
|
358
|
-
...prepared.options,
|
|
359
|
-
publish: input.publish ?? false,
|
|
360
|
-
nodeId: config.nodeId,
|
|
361
|
-
label: input.label,
|
|
362
|
-
})
|
|
363
|
-
await writeReport(db, result.check.findings, { source: 'transfer', scope: set.manifest.id })
|
|
364
|
-
const outcome: ContentImportResult = {
|
|
365
|
-
bundle: summarise(set),
|
|
366
|
-
backup,
|
|
367
|
-
runId: result.runId,
|
|
368
|
-
check: result.check,
|
|
369
|
-
blobs: null,
|
|
370
|
-
published: result.published.length,
|
|
371
|
-
publishFailures: result.publishFailures,
|
|
372
|
-
problems: prepared.problems,
|
|
373
|
-
sections,
|
|
374
|
-
}
|
|
375
|
-
if (!result.runId) return outcome
|
|
376
|
-
// The backups become the run's, so `content revert` can find them.
|
|
377
|
-
if (staging) outcome.replacedFilesKept = promoteBackups(config, staging, result.runId)
|
|
378
|
-
if (blobs.size > 0) outcome.blobs = await placeBlobs(await mediaStoreFor(config), blobs)
|
|
379
|
-
await recordInLedger(db, {
|
|
380
|
-
name: `content import ${set.manifest.id}`,
|
|
381
|
-
kind: 'transfer',
|
|
382
|
-
durationMs: Math.round(performance.now() - started),
|
|
383
|
-
appliedBy: config.nodeId,
|
|
384
|
-
note: `run ${result.runId}`,
|
|
385
|
-
})
|
|
386
|
-
return outcome
|
|
133
|
+
return await installBundleContent(db, config, input)
|
|
387
134
|
} finally {
|
|
388
135
|
await db.close()
|
|
389
136
|
}
|
|
@@ -396,24 +143,38 @@ export interface ContentExportInput {
|
|
|
396
143
|
drafts?: boolean
|
|
397
144
|
withBlobs?: boolean
|
|
398
145
|
withBlueprints?: boolean
|
|
146
|
+
/**
|
|
147
|
+
* The private package the export is written as. The name defaults to one
|
|
148
|
+
* derived from the bundle's id, the version to `1.0.0`.
|
|
149
|
+
*/
|
|
150
|
+
package?: { name?: string; version?: string }
|
|
399
151
|
}
|
|
400
152
|
|
|
401
153
|
export interface ContentExportResult {
|
|
402
154
|
bundleId: string
|
|
155
|
+
/** The package the files make up. */
|
|
156
|
+
package: { name: string; version: string }
|
|
403
157
|
snapshot: string
|
|
404
158
|
counts: Record<string, number>
|
|
405
159
|
expected: { key: string; name: string; why: string }[]
|
|
406
|
-
/** The
|
|
160
|
+
/** The package's files — `package.json` and the payload under `content/` — for the caller to place. */
|
|
407
161
|
files: { path: string; bytes?: Uint8Array; text?: string }[]
|
|
408
162
|
blobs: { carried: number; bytes: number; missing: string[]; notCarried: number }
|
|
409
163
|
}
|
|
410
164
|
|
|
411
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* Writes nothing: returns the export as a private package's files, for the
|
|
167
|
+
* caller to place. An environment's export is a bundle like any other, but one
|
|
168
|
+
* applied from its directory and never published or depended on.
|
|
169
|
+
*/
|
|
412
170
|
export async function contentExport(
|
|
413
171
|
config: CromboloConfig,
|
|
414
172
|
input: ContentExportInput,
|
|
415
173
|
): Promise<ContentExportResult> {
|
|
416
174
|
if (input.roots.length === 0) throw new CommandError('An export needs at least one root.')
|
|
175
|
+
const requested = input.package?.name
|
|
176
|
+
if (requested !== undefined && !isPackageName(requested))
|
|
177
|
+
throw new CommandError(`"${requested}" is not a package name npm accepts.`)
|
|
417
178
|
const { db } = await bootstrapDatabase(config)
|
|
418
179
|
try {
|
|
419
180
|
// A path is resolved against the tree it is exported from, and the key is
|
|
@@ -443,8 +204,13 @@ export async function contentExport(
|
|
|
443
204
|
carried = read.bytes
|
|
444
205
|
missing = read.missing
|
|
445
206
|
}
|
|
207
|
+
const pkg = {
|
|
208
|
+
name: requested ?? packageNameFor(`export-${set.manifest.id}`),
|
|
209
|
+
version: input.package?.version ?? '1.0.0',
|
|
210
|
+
}
|
|
446
211
|
return {
|
|
447
212
|
bundleId: set.manifest.id,
|
|
213
|
+
package: pkg,
|
|
448
214
|
snapshot: set.manifest.snapshot,
|
|
449
215
|
counts: { ...set.manifest.counts },
|
|
450
216
|
expected: set.manifest.dependencies.expected.map((e) => ({
|
|
@@ -452,7 +218,13 @@ export async function contentExport(
|
|
|
452
218
|
name: e.name,
|
|
453
219
|
why: e.why,
|
|
454
220
|
})),
|
|
455
|
-
files: writeBundle(set, carried),
|
|
221
|
+
files: writeBundlePackage(writeBundle(set, carried), {
|
|
222
|
+
...pkg,
|
|
223
|
+
id: bundleIdFor(pkg.name.replace(/^@[^/]+\//, '')),
|
|
224
|
+
label: `Content from ${config.siteName}`,
|
|
225
|
+
private: true,
|
|
226
|
+
peerRange: `^${VERSION}`,
|
|
227
|
+
}),
|
|
456
228
|
blobs: {
|
|
457
229
|
carried: carried.size,
|
|
458
230
|
bytes: [...carried.values()].reduce((total, file) => total + file.length, 0),
|
|
@@ -465,7 +237,21 @@ export async function contentExport(
|
|
|
465
237
|
}
|
|
466
238
|
}
|
|
467
239
|
|
|
240
|
+
/** The wired bundles' chains, checked as boot checks them, or a refusal naming every problem. */
|
|
241
|
+
function chainsFor(config: CromboloConfig): BundleChain[] {
|
|
242
|
+
try {
|
|
243
|
+
return bundleChainsFor(config)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error instanceof BundleConfigurationError)
|
|
246
|
+
throw new CommandError('The configured bundles cannot be used.', error.problems)
|
|
247
|
+
throw error
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const flat = (sql: string) => sql.replace(/\s+/g, ' ').trim()
|
|
252
|
+
|
|
468
253
|
async function upgradeContext(config: CromboloConfig, set: Record<string, unknown>) {
|
|
254
|
+
const chains = chainsFor(config)
|
|
469
255
|
const db = await connect({ file: config.sqliteFile })
|
|
470
256
|
const node = { nodeId: config.nodeId, revision: config.schemaRevision }
|
|
471
257
|
const loaded = () => loadSchemaDirectory(config.schemaDir)
|
|
@@ -476,33 +262,45 @@ async function upgradeContext(config: CromboloConfig, set: Record<string, unknow
|
|
|
476
262
|
migrations: await loadValueMigrations(config.schemaDir),
|
|
477
263
|
backOfficePath: config.backOfficePath,
|
|
478
264
|
})
|
|
479
|
-
return { db, node, loaded, syncNode, checkOptions }
|
|
265
|
+
return { db, node, loaded, syncNode, checkOptions, chains }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface PlannedStepResult {
|
|
269
|
+
name: string
|
|
270
|
+
kind: string
|
|
271
|
+
release: string | null
|
|
272
|
+
statements: string[]
|
|
273
|
+
unplannable: string | null
|
|
480
274
|
}
|
|
481
275
|
|
|
482
276
|
export interface UpgradePlanResult {
|
|
483
|
-
steps:
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
release: string | null
|
|
487
|
-
statements: string[]
|
|
488
|
-
unplannable: string | null
|
|
489
|
-
}[]
|
|
277
|
+
steps: PlannedStepResult[]
|
|
278
|
+
/** Each wired bundle that owns tables, with the steps it would run after core's. */
|
|
279
|
+
bundles: { id: string; name: string; steps: PlannedStepResult[] }[]
|
|
490
280
|
site: { checked: boolean; action: string | null; classification: string | null }
|
|
491
281
|
}
|
|
492
282
|
|
|
283
|
+
const plannedStep = (step: PlannedStep): PlannedStepResult => ({
|
|
284
|
+
name: step.name,
|
|
285
|
+
kind: step.kind,
|
|
286
|
+
release: step.release ?? null,
|
|
287
|
+
statements: (step.statements ?? []).map(flat),
|
|
288
|
+
unplannable: step.unplannable ?? null,
|
|
289
|
+
})
|
|
290
|
+
|
|
493
291
|
/** The DDL every pending framework step would run, and what the site's schema would do. Writes nothing. */
|
|
494
292
|
export async function upgradePlan(config: CromboloConfig): Promise<UpgradePlanResult> {
|
|
495
|
-
const { db, node } = await upgradeContext(config, {})
|
|
293
|
+
const { db, node, chains } = await upgradeContext(config, {})
|
|
496
294
|
try {
|
|
497
295
|
const steps = await planUpgrade(db, cromboloPlan)
|
|
296
|
+
const bundles = await planBundleUpgrades(db, chains)
|
|
498
297
|
const check = await checkSchemaDirectory(db, config, node)
|
|
499
298
|
return {
|
|
500
|
-
steps: steps.map(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
unplannable: step.unplannable ?? null,
|
|
299
|
+
steps: steps.map(plannedStep),
|
|
300
|
+
bundles: bundles.map((bundle) => ({
|
|
301
|
+
id: bundle.id,
|
|
302
|
+
name: bundle.name,
|
|
303
|
+
steps: bundle.steps.map(plannedStep),
|
|
506
304
|
})),
|
|
507
305
|
site: {
|
|
508
306
|
checked: check.siteChecked,
|
|
@@ -520,11 +318,17 @@ export async function upgradeCheck(
|
|
|
520
318
|
config: CromboloConfig,
|
|
521
319
|
input: { set?: Record<string, unknown> } = {},
|
|
522
320
|
) {
|
|
523
|
-
const { db, node } = await upgradeContext(config, input.set ?? {})
|
|
321
|
+
const { db, node, chains } = await upgradeContext(config, input.set ?? {})
|
|
524
322
|
try {
|
|
525
323
|
const report = await checkSchemaDirectory(db, config, node, { set: input.set ?? {} })
|
|
526
324
|
if (report.siteChecked) await writeReport(db, report.findings, { source: 'upgrade' })
|
|
325
|
+
const bundles = await pendingBundleSteps(db, chains)
|
|
527
326
|
return {
|
|
327
|
+
bundles: bundles.map((bundle) => ({
|
|
328
|
+
id: bundle.id,
|
|
329
|
+
name: bundle.name,
|
|
330
|
+
pending: bundle.pending.map((step) => ({ name: step.name, kind: step.kind ?? 'expand' })),
|
|
331
|
+
})),
|
|
528
332
|
siteChecked: report.siteChecked,
|
|
529
333
|
classification: report.classification,
|
|
530
334
|
findings: report.findings,
|
|
@@ -542,9 +346,12 @@ export async function upgradeFix(
|
|
|
542
346
|
input: { set?: Record<string, unknown>; backupTaken?: boolean } = {},
|
|
543
347
|
) {
|
|
544
348
|
const backup = await backupBefore(config, { reason: 'fix', backupTaken: input.backupTaken })
|
|
545
|
-
const { db, loaded, syncNode, checkOptions } = await upgradeContext(
|
|
349
|
+
const { db, loaded, syncNode, checkOptions, chains } = await upgradeContext(
|
|
350
|
+
config,
|
|
351
|
+
input.set ?? {},
|
|
352
|
+
)
|
|
546
353
|
try {
|
|
547
|
-
await installDatabase(db, config)
|
|
354
|
+
await installDatabase(db, config, chains)
|
|
548
355
|
const fix = await runFix(db, loaded(), syncNode(), {
|
|
549
356
|
...(await checkOptions()),
|
|
550
357
|
by: config.nodeId,
|
|
@@ -574,6 +381,8 @@ export type UpgradeRunResult =
|
|
|
574
381
|
action: 'upgraded'
|
|
575
382
|
backup: BackupResult
|
|
576
383
|
frameworkApplied: string[]
|
|
384
|
+
/** Each bundle's steps that ran, after core's. */
|
|
385
|
+
bundlesApplied: { id: string; applied: string[] }[]
|
|
577
386
|
schema: { action: string; version: string | null; revision: string | null } | null
|
|
578
387
|
}
|
|
579
388
|
|
|
@@ -583,9 +392,19 @@ export async function upgradeRun(
|
|
|
583
392
|
input: { force?: string; set?: Record<string, unknown>; backupTaken?: boolean } = {},
|
|
584
393
|
): Promise<UpgradeRunResult> {
|
|
585
394
|
const backup = await backupBefore(config, { reason: 'upgrade', backupTaken: input.backupTaken })
|
|
586
|
-
const { db, loaded, syncNode, checkOptions } = await upgradeContext(
|
|
395
|
+
const { db, loaded, syncNode, checkOptions, chains } = await upgradeContext(
|
|
396
|
+
config,
|
|
397
|
+
input.set ?? {},
|
|
398
|
+
)
|
|
587
399
|
try {
|
|
588
|
-
|
|
400
|
+
// Read before anything runs: `installDatabase` applies core's steps and then
|
|
401
|
+
// each bundle's, so this is what the upgrade itself applied.
|
|
402
|
+
const corePending = (await pendingMigrations(db, cromboloPlan)).map((step) => step.name)
|
|
403
|
+
const bundlesPending = await pendingBundleSteps(db, chains)
|
|
404
|
+
await installDatabase(db, config, chains)
|
|
405
|
+
const bundlesApplied = bundlesPending
|
|
406
|
+
.filter((bundle) => bundle.pending.length > 0)
|
|
407
|
+
.map((bundle) => ({ id: bundle.id, applied: bundle.pending.map((step) => step.name) }))
|
|
589
408
|
const result = await runUpgrade(db, loaded(), syncNode(), {
|
|
590
409
|
...(await checkOptions()),
|
|
591
410
|
by: config.nodeId,
|
|
@@ -598,11 +417,14 @@ export async function upgradeRun(
|
|
|
598
417
|
backup,
|
|
599
418
|
reasons: result.reasons.map((f) => ({ kind: f.kind, message: f.message, link: f.link })),
|
|
600
419
|
}
|
|
601
|
-
|
|
420
|
+
const frameworkApplied = [...corePending, ...result.frameworkApplied]
|
|
421
|
+
if (result.action === 'nothing' && frameworkApplied.length === 0 && bundlesApplied.length === 0)
|
|
422
|
+
return { action: 'nothing', backup }
|
|
602
423
|
return {
|
|
603
424
|
action: 'upgraded',
|
|
604
425
|
backup,
|
|
605
|
-
frameworkApplied
|
|
426
|
+
frameworkApplied,
|
|
427
|
+
bundlesApplied,
|
|
606
428
|
schema: result.sync
|
|
607
429
|
? {
|
|
608
430
|
action: result.sync.action,
|
|
@@ -746,6 +568,44 @@ export async function backup(
|
|
|
746
568
|
})
|
|
747
569
|
}
|
|
748
570
|
|
|
571
|
+
export interface CommandAudit {
|
|
572
|
+
category: AuditCategory
|
|
573
|
+
action: string
|
|
574
|
+
/** The command as typed, without its arguments, which can hold a password. */
|
|
575
|
+
command: string
|
|
576
|
+
entityType?: string
|
|
577
|
+
entityKey?: string
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Records a command that changed this environment, in the audit log, as the
|
|
582
|
+
* account that ran it — `CROMBOLO_ACTOR` when a pipeline names who it runs for.
|
|
583
|
+
* Never fails the command: what it records has already happened.
|
|
584
|
+
*/
|
|
585
|
+
export async function recordCommand(config: CromboloConfig, entry: CommandAudit): Promise<void> {
|
|
586
|
+
let actor = Bun.env.CROMBOLO_ACTOR
|
|
587
|
+
try {
|
|
588
|
+
actor ??= userInfo().username
|
|
589
|
+
} catch {
|
|
590
|
+
actor ??= undefined
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
await withDatabase(config, (db) =>
|
|
594
|
+
recordAudit(db, {
|
|
595
|
+
category: entry.category,
|
|
596
|
+
action: entry.action,
|
|
597
|
+
source: 'cli',
|
|
598
|
+
userName: actor ?? null,
|
|
599
|
+
entityType: entry.entityType ?? null,
|
|
600
|
+
entityKey: entry.entityKey ?? null,
|
|
601
|
+
detail: entry.command,
|
|
602
|
+
}),
|
|
603
|
+
)
|
|
604
|
+
} catch {
|
|
605
|
+
// No database to write to, or one without the log yet: the command still happened.
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
749
609
|
async function withDatabase<T>(config: CromboloConfig, use: (db: Db) => Promise<T>): Promise<T> {
|
|
750
610
|
const db = await connect({ file: config.sqliteFile })
|
|
751
611
|
try {
|
|
@@ -773,3 +633,54 @@ export function resumeEditing(config: CromboloConfig): Promise<void> {
|
|
|
773
633
|
export function editingPaused(config: CromboloConfig): Promise<WritesPaused | undefined> {
|
|
774
634
|
return withDatabase(config, readWritesPaused)
|
|
775
635
|
}
|
|
636
|
+
|
|
637
|
+
export interface BundlePurgePlan {
|
|
638
|
+
id: string
|
|
639
|
+
/** The tables that would be dropped, as the database names them. */
|
|
640
|
+
tables: string[]
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Refuses a purge of a bundle the site still wires: its code would recreate what was dropped. */
|
|
644
|
+
function refuseWired(config: CromboloConfig, id: string): void {
|
|
645
|
+
if (config.bundles.some((bundle) => bundle.id === id))
|
|
646
|
+
throw new CommandError(
|
|
647
|
+
`The bundle "${id}" is still wired in crombolo.config.ts. Take it out of \`bundles\` and deploy that first.`,
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Read-only: what purging a bundle would drop. */
|
|
652
|
+
export async function bundlePurgePlan(
|
|
653
|
+
config: CromboloConfig,
|
|
654
|
+
id: string,
|
|
655
|
+
): Promise<BundlePurgePlan> {
|
|
656
|
+
refuseWired(config, id)
|
|
657
|
+
const db = await connect({ file: config.sqliteFile })
|
|
658
|
+
try {
|
|
659
|
+
return { id, tables: await bundleTablesIn(db, id) }
|
|
660
|
+
} finally {
|
|
661
|
+
await db.close()
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Backs up, then drops every table under the bundle's prefix and forgets its
|
|
667
|
+
* state, ledgered as a `purge` against the bundle.
|
|
668
|
+
*/
|
|
669
|
+
export async function bundlePurge(
|
|
670
|
+
config: CromboloConfig,
|
|
671
|
+
id: string,
|
|
672
|
+
input: { backupTaken?: boolean } = {},
|
|
673
|
+
): Promise<{ backup: BackupResult; dropped: string[] }> {
|
|
674
|
+
refuseWired(config, id)
|
|
675
|
+
const backup = await backupBefore(config, { reason: 'purge', backupTaken: input.backupTaken })
|
|
676
|
+
const db = await connect({ file: config.sqliteFile })
|
|
677
|
+
try {
|
|
678
|
+
const dropped = await purgeBundleTables(db, id, {
|
|
679
|
+
by: config.nodeId,
|
|
680
|
+
stateKey: bundleStateKey(id),
|
|
681
|
+
})
|
|
682
|
+
return { backup, dropped }
|
|
683
|
+
} finally {
|
|
684
|
+
await db.close()
|
|
685
|
+
}
|
|
686
|
+
}
|