-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathloader.ts
More file actions
203 lines (184 loc) · 7.65 KB
/
loader.ts
File metadata and controls
203 lines (184 loc) · 7.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import type { Contract } from '@prisma-next/contract/types';
import { MigrationToolsError } from '../errors';
import { readMigrationsDir } from '../io';
import { readContractSpaceContract } from '../read-contract-space-contract';
import { readContractSpaceHeadRef } from '../read-contract-space-head-ref';
import { HEAD_REF_NAME, type RefLoadProblem, readRefsTolerant } from '../refs';
import {
APP_SPACE_ID,
isValidSpaceId,
RESERVED_SPACE_SUBDIR_NAMES,
spaceMigrationDirectory,
spaceRefsDirectory,
} from '../space-layout';
import { listContractSpaceDirectories } from '../verify-contract-spaces';
import { createContractSpaceAggregate, createContractSpaceMember } from './aggregate';
import { computeIntegrityViolations, type IntegritySpaceState } from './check-integrity';
import type { ContractSpaceAggregate } from './types';
export type { DeclaredExtensionEntry } from '../integrity-violation';
/**
* Inputs for {@link loadContractSpaceAggregate}.
*
* Construction reads migration **state** from disk (`migrations/<space>/`
* packages + refs + head refs). The app's *live* contract is not a disk
* artefact — in Prisma Next it is always compiled from the project's
* central contract, so the caller always has it and threads it in as
* `appContract`. `deserializeContract` is held and called lazily only for
* the on-disk extension contracts (`migrations/<ext>/contract.json`).
*/
export interface LoadAggregateInput {
readonly migrationsDir: string;
readonly deserializeContract: (raw: unknown) => Contract;
readonly appContract: Contract;
}
/**
* Build a tolerant, queryable {@link ContractSpaceAggregate} from on-disk
* migration state plus the caller's live app contract.
*
* Building **never throws on disk content**: a hash- or
* invariants-mismatched package is retained, an unparseable package is
* omitted, a missing extension head ref leaves `headRef: null`, and an
* unreadable on-disk contract defers its failure to `member.contract()`.
* Every such problem is judged by {@link ContractSpaceAggregate.checkIntegrity}
* rather than aborting the load. The only rejections are catastrophic I/O
* (a `migrations/` that exists but is unreadable for reasons other than
* absence).
*
* The app space's head ref is synthesised from the live contract's
* storage hash (the app contract is authored independently of the
* migration graph), and `app.contract()` returns the supplied contract.
* Extension spaces read their contract, refs, and head ref from disk.
*/
export async function loadContractSpaceAggregate(
input: LoadAggregateInput,
): Promise<ContractSpaceAggregate> {
const { migrationsDir, deserializeContract, appContract } = input;
const targetId = appContract.target;
const appState = await loadAppSpace(migrationsDir, appContract, deserializeContract);
const extensionStates = await loadExtensionSpaces(migrationsDir, deserializeContract);
const spaces: readonly IntegritySpaceState[] = [appState, ...extensionStates];
return createContractSpaceAggregate({
targetId,
app: appState.member,
extensions: extensionStates.map((state) => state.member),
checkIntegrity: (opts) => computeIntegrityViolations({ targetId, spaces }, opts),
});
}
async function loadAppSpace(
migrationsDir: string,
appContract: Contract,
deserializeContract: (raw: unknown) => Contract,
): Promise<IntegritySpaceState> {
const spaceDir = spaceMigrationDirectory(migrationsDir, APP_SPACE_ID);
const { packages, problems } = await readMigrationsDir(spaceDir);
const { refs, problems: refProblems } = await readRefsTolerant(spaceRefsDirectory(spaceDir));
const member = createContractSpaceMember({
spaceId: APP_SPACE_ID,
packages,
refs,
headRef: { hash: appContract.storage.storageHash, invariants: [] },
refsDir: spaceRefsDirectory(spaceDir),
resolveContract: () => appContract,
deserializeContract,
});
// The app head ref is synthesised from the live contract, so there is
// no on-disk head.json to be missing or corrupt for it.
return { member, problems, refProblems, headRefProblem: null, isApp: true };
}
async function loadExtensionSpaces(
migrationsDir: string,
deserializeContract: (raw: unknown) => Contract,
): Promise<readonly IntegritySpaceState[]> {
const candidateDirs = await listContractSpaceDirectories(migrationsDir);
const extensionIds = candidateDirs
.filter((name) => name !== APP_SPACE_ID)
.filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name))
.filter(isValidSpaceId)
.sort();
const states: IntegritySpaceState[] = [];
for (const spaceId of extensionIds) {
states.push(await loadExtensionSpace(migrationsDir, spaceId, deserializeContract));
}
return states;
}
async function loadExtensionSpace(
migrationsDir: string,
spaceId: string,
deserializeContract: (raw: unknown) => Contract,
): Promise<IntegritySpaceState> {
const spaceDir = spaceMigrationDirectory(migrationsDir, spaceId);
const { packages, problems } = await readMigrationsDir(spaceDir);
const { refs, problems: refProblems } = await readRefsTolerant(spaceRefsDirectory(spaceDir));
const { headRef, problem: headRefProblem } = await readHeadRefTolerant(migrationsDir, spaceId);
const rawContract = await readRawContractDeferred(migrationsDir, spaceId);
const member = createContractSpaceMember({
spaceId,
packages,
refs,
headRef,
refsDir: spaceRefsDirectory(spaceDir),
resolveContract: () => deserializeContract(rawContract()),
deserializeContract,
});
return { member, problems, refProblems, headRefProblem, isApp: false };
}
/**
* The result of resolving an extension's `refs/head.json`: the parsed
* head ref (or `null` when the file is absent or corrupt) plus a problem
* when the file exists but cannot be parsed.
*/
interface HeadRefReadResult {
readonly headRef: Awaited<ReturnType<typeof readContractSpaceHeadRef>>;
readonly problem: RefLoadProblem | null;
}
/**
* Read an extension's head ref, distinguishing a *genuinely absent*
* `head.json` (`headRef: null`, no problem — judged `headRefMissing`)
* from one that *exists but cannot be parsed* (`headRef: null` plus a
* problem — judged `refUnreadable`, not `headRefMissing`).
* `readContractSpaceHeadRef` already returns `null` only for ENOENT and
* throws for unparseable / schema-invalid content, so the throw is the
* corruption signal. Construction never throws on disk content.
*/
function isToleratedRefHeadReadError(error: unknown): boolean {
if (MigrationToolsError.is(error)) return true;
if (!(error instanceof Error)) return false;
const code = (error as NodeJS.ErrnoException).code;
return code === 'ENOENT' || code === 'EISDIR';
}
async function readHeadRefTolerant(
migrationsDir: string,
spaceId: string,
): Promise<HeadRefReadResult> {
try {
const headRef = await readContractSpaceHeadRef(migrationsDir, spaceId);
return { headRef, problem: null };
} catch (error) {
if (!isToleratedRefHeadReadError(error)) {
throw error;
}
return { headRef: null, problem: { refName: HEAD_REF_NAME, detail: detailOf(error) } };
}
}
function detailOf(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* Read the raw on-disk contract eagerly (cheap I/O) but defer its
* (throwing) failure to call time, so a missing or unparseable
* `contract.json` becomes a `contract()` throw — surfaced as
* `contractUnreadable` — rather than a construction failure.
*/
async function readRawContractDeferred(
migrationsDir: string,
spaceId: string,
): Promise<() => unknown> {
try {
const raw = await readContractSpaceContract(migrationsDir, spaceId);
return () => raw;
} catch (error) {
return () => {
throw error;
};
}
}