@opengeni/db 0.7.3 → 0.9.3
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/dist/{chunk-B22X3IEZ.js → chunk-4LG5NBTC.js} +512 -32
- package/dist/chunk-4LG5NBTC.js.map +1 -0
- package/dist/{chunk-YFQ7SGE4.js → chunk-BMFDXFPA.js} +23 -1
- package/dist/chunk-BMFDXFPA.js.map +1 -0
- package/dist/chunk-KW526IJA.js +127 -0
- package/dist/chunk-KW526IJA.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6068 -2090
- package/dist/index.js.map +1 -1
- package/dist/migrate.d.ts +29 -4
- package/dist/migrate.js +3 -1
- package/dist/provision-roles.d.ts +721 -46
- package/dist/provision-roles.js +1 -1
- package/dist/{schema-BN5mB9xZ.d.ts → schema-CdPGTHlD.d.ts} +2118 -119
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +17 -1
- package/drizzle/0064_rotation_strategy_sharded_backfill.sql +15 -0
- package/drizzle/0065_session_attempt_quiescence.sql +26 -0
- package/drizzle/0066_session_interruption_attempt_lookup.sql +4 -0
- package/drizzle/0067_session_event_payload_bounds.sql +209 -0
- package/drizzle/0068_workspace_control_event_bounds.sql +134 -0
- package/drizzle/0069_session_event_history_backfill.sql +32 -0
- package/drizzle/0070_session_event_type_sequence_lookup.sql +4 -0
- package/drizzle/0071_session_event_monitoring_tail.sql +10 -0
- package/drizzle/0072_sessions_workspace_created_id_idx.sql +4 -0
- package/drizzle/0073_sessions_workspace_updated_id_idx.sql +4 -0
- package/drizzle/0074_session_activity_revisions.sql +72 -0
- package/drizzle/0075_sessions_workspace_activity_revision_idx.sql +4 -0
- package/drizzle/0076_session_workflow_wake_acl.sql +13 -0
- package/drizzle/0077_session_attempt_latest_lookup.sql +4 -0
- package/drizzle/0094_quarantine_credential_bearing_catalog_urls.sql +51 -0
- package/drizzle/0095_github_existing_installations.sql +84 -0
- package/drizzle/0096_session_turn_initiators.sql +97 -0
- package/drizzle/0097_host_export_outbox.sql +1220 -0
- package/drizzle/0098_usage_events_workspace_session_idx.sql +4 -0
- package/drizzle/0099_session_human_input_attempt_owner_index.sql +6 -0
- package/drizzle/0100_session_human_input_requests.sql +89 -0
- package/drizzle/0101_session_mcp_connection_refs.sql +30 -0
- package/drizzle/0102_session_command_receipt_service_actor.sql +16 -0
- package/drizzle/0103_host_export_root_session.sql +166 -0
- package/drizzle/0104_host_export_root_session_backfill.sql +27 -0
- package/drizzle/0105_session_turn_instructions.sql +9 -0
- package/package.json +4 -4
- package/src/connection-token-resolver.ts +287 -1
- package/src/event-payload-sanitizer.ts +57 -19
- package/src/index.ts +5377 -1125
- package/src/memory-domain.ts +1 -1
- package/src/migrate.ts +86 -23
- package/src/persistence-errors.ts +252 -0
- package/src/provision-roles.ts +42 -0
- package/src/schema.ts +552 -34
- package/src/session-control.ts +518 -38
- package/src/session-queue-commands.ts +308 -57
- package/src/session-tool-call-settlement.ts +58 -7
- package/src/turn-initiator.ts +155 -0
- package/dist/chunk-7LDU7F5P.js +0 -80
- package/dist/chunk-7LDU7F5P.js.map +0 -1
- package/dist/chunk-B22X3IEZ.js.map +0 -1
- package/dist/chunk-YFQ7SGE4.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionEvent } from "@opengeni/contracts";
|
|
2
|
+
import { boundModelToolOutputItem } from "@opengeni/codex";
|
|
2
3
|
import { and, asc, eq, sql } from "drizzle-orm";
|
|
3
4
|
import type { Database } from "./index";
|
|
4
5
|
import { sanitizeEventPayload, sanitizeModelPayload } from "./event-payload-sanitizer";
|
|
@@ -129,14 +130,17 @@ export async function closePendingSessionToolCallsInTransaction(
|
|
|
129
130
|
if (pending.length === 0) return { sequence: input.sequence, events: [], closed: 0 };
|
|
130
131
|
|
|
131
132
|
const history = await tx
|
|
132
|
-
.select({
|
|
133
|
+
.select({
|
|
134
|
+
position: schema.sessionHistoryItems.position,
|
|
135
|
+
item: schema.sessionHistoryItems.item,
|
|
136
|
+
active: schema.sessionHistoryItems.active,
|
|
137
|
+
})
|
|
133
138
|
.from(schema.sessionHistoryItems)
|
|
134
139
|
.where(
|
|
135
140
|
and(
|
|
136
141
|
eq(schema.sessionHistoryItems.workspaceId, input.workspaceId),
|
|
137
142
|
eq(schema.sessionHistoryItems.sessionId, input.sessionId),
|
|
138
143
|
eq(schema.sessionHistoryItems.turnId, input.turnId),
|
|
139
|
-
eq(schema.sessionHistoryItems.active, true),
|
|
140
144
|
),
|
|
141
145
|
)
|
|
142
146
|
.orderBy(asc(schema.sessionHistoryItems.position));
|
|
@@ -149,6 +153,20 @@ export async function closePendingSessionToolCallsInTransaction(
|
|
|
149
153
|
eq(schema.sessionHistoryItems.sessionId, input.sessionId),
|
|
150
154
|
),
|
|
151
155
|
);
|
|
156
|
+
const existingOutputEvents = await tx
|
|
157
|
+
.select({ callId: sql<string | null>`${schema.sessionEvents.payload} ->> 'id'` })
|
|
158
|
+
.from(schema.sessionEvents)
|
|
159
|
+
.where(
|
|
160
|
+
and(
|
|
161
|
+
eq(schema.sessionEvents.workspaceId, input.workspaceId),
|
|
162
|
+
eq(schema.sessionEvents.sessionId, input.sessionId),
|
|
163
|
+
eq(schema.sessionEvents.turnId, input.turnId),
|
|
164
|
+
eq(schema.sessionEvents.type, "agent.toolCall.output"),
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
const projectedCallIds = new Set(
|
|
168
|
+
existingOutputEvents.flatMap(({ callId }) => (callId ? [callId] : [])),
|
|
169
|
+
);
|
|
152
170
|
let nextPosition = Math.floor(Number(maxPosition)) + 1;
|
|
153
171
|
let sequence = input.sequence;
|
|
154
172
|
const historyValues: Array<typeof schema.sessionHistoryItems.$inferInsert> = [];
|
|
@@ -160,7 +178,23 @@ export async function closePendingSessionToolCallsInTransaction(
|
|
|
160
178
|
);
|
|
161
179
|
const existingResult = resultType
|
|
162
180
|
? history.find(
|
|
163
|
-
({ item }) =>
|
|
181
|
+
({ item, position }) =>
|
|
182
|
+
position > (existingCall?.position ?? Number.MAX_SAFE_INTEGER) &&
|
|
183
|
+
historyItemType(item) === resultType &&
|
|
184
|
+
historyCallId(item) === call.callId,
|
|
185
|
+
)
|
|
186
|
+
: undefined;
|
|
187
|
+
const activeCall = history.find(
|
|
188
|
+
({ item, active }) =>
|
|
189
|
+
active && historyItemType(item) === call.callType && historyCallId(item) === call.callId,
|
|
190
|
+
);
|
|
191
|
+
const activeResult = resultType
|
|
192
|
+
? history.find(
|
|
193
|
+
({ item, active, position }) =>
|
|
194
|
+
active &&
|
|
195
|
+
position > (activeCall?.position ?? Number.MAX_SAFE_INTEGER) &&
|
|
196
|
+
historyItemType(item) === resultType &&
|
|
197
|
+
historyCallId(item) === call.callId,
|
|
164
198
|
)
|
|
165
199
|
: undefined;
|
|
166
200
|
const interruptedResult = interruptedToolCallResult({
|
|
@@ -173,6 +207,12 @@ export async function closePendingSessionToolCallsInTransaction(
|
|
|
173
207
|
call,
|
|
174
208
|
existingCall,
|
|
175
209
|
existingResult,
|
|
210
|
+
activeCall,
|
|
211
|
+
activeResult,
|
|
212
|
+
completeDurablePair: Boolean(existingCall && existingResult),
|
|
213
|
+
supersededDurablePair: Boolean(
|
|
214
|
+
existingCall && existingResult && (!existingCall.active || !existingResult.active),
|
|
215
|
+
),
|
|
176
216
|
rawCallIsValid: historyItemType(call.callItem) === call.callType,
|
|
177
217
|
result: existingResult?.item ?? call.resultItem ?? interruptedResult,
|
|
178
218
|
interrupted: !existingResult && !call.resultItem,
|
|
@@ -181,8 +221,8 @@ export async function closePendingSessionToolCallsInTransaction(
|
|
|
181
221
|
|
|
182
222
|
for (const resolution of resolutions) {
|
|
183
223
|
if (
|
|
184
|
-
!resolution.
|
|
185
|
-
!resolution.
|
|
224
|
+
!resolution.completeDurablePair &&
|
|
225
|
+
!resolution.activeCall &&
|
|
186
226
|
resolution.result &&
|
|
187
227
|
resolution.rawCallIsValid
|
|
188
228
|
) {
|
|
@@ -203,17 +243,28 @@ export async function closePendingSessionToolCallsInTransaction(
|
|
|
203
243
|
(right.call.resultRecordedAt?.getTime() ?? Number.MAX_SAFE_INTEGER),
|
|
204
244
|
);
|
|
205
245
|
for (const resolution of orderedResults) {
|
|
206
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
!resolution.completeDurablePair &&
|
|
248
|
+
!resolution.activeResult &&
|
|
249
|
+
resolution.result &&
|
|
250
|
+
resolution.rawCallIsValid
|
|
251
|
+
) {
|
|
207
252
|
historyValues.push({
|
|
208
253
|
accountId: input.accountId,
|
|
209
254
|
workspaceId: input.workspaceId,
|
|
210
255
|
sessionId: input.sessionId,
|
|
211
256
|
turnId: input.turnId,
|
|
212
257
|
position: nextPosition++,
|
|
213
|
-
item: sanitizeModelPayload(resolution.result),
|
|
258
|
+
item: sanitizeModelPayload(boundModelToolOutputItem(resolution.result)),
|
|
214
259
|
active: true,
|
|
215
260
|
});
|
|
216
261
|
}
|
|
262
|
+
// A pair superseded by compaction was already projected during its live
|
|
263
|
+
// response. Reactivating or re-emitting it would undo the checkpoint and
|
|
264
|
+
// duplicate the UI output. An active complete pair is different: it can be
|
|
265
|
+
// the crash point after model-memory persistence but before event publish,
|
|
266
|
+
// so it keeps the existing durable recovery projection below.
|
|
267
|
+
if (resolution.supersededDurablePair || projectedCallIds.has(resolution.call.callId)) continue;
|
|
217
268
|
eventValues.push({
|
|
218
269
|
accountId: input.accountId,
|
|
219
270
|
workspaceId: input.workspaceId,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UNATTRIBUTED_LEGACY_INITIATOR_SUBJECT_ID,
|
|
3
|
+
type TurnInitiator,
|
|
4
|
+
type TurnInitiatorContext,
|
|
5
|
+
} from "@opengeni/contracts";
|
|
6
|
+
import { and, eq } from "drizzle-orm";
|
|
7
|
+
import type { Database } from "./index";
|
|
8
|
+
import type { SessionCommandActor } from "./session-control";
|
|
9
|
+
import * as schema from "./schema";
|
|
10
|
+
|
|
11
|
+
export const UNATTRIBUTED_LEGACY_INITIATOR: TurnInitiator = {
|
|
12
|
+
kind: "service",
|
|
13
|
+
subjectId: UNATTRIBUTED_LEGACY_INITIATOR_SUBJECT_ID,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type FrozenTurnInitiator = {
|
|
17
|
+
initiator: TurnInitiator;
|
|
18
|
+
context: TurnInitiatorContext;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const MAX_AGENT_PROVENANCE_HOPS = 32;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bound agent provenance without discarding its causal authority. Once the
|
|
25
|
+
* chain exceeds the cap, retain the first hop and the newest hops in order;
|
|
26
|
+
* consumers can still identify the root attempt while recent diagnostics stay
|
|
27
|
+
* useful. The omitted middle is signalled separately by `viaTruncated`.
|
|
28
|
+
*/
|
|
29
|
+
export function clipAgentProvenanceHops(
|
|
30
|
+
hops: Array<Record<string, unknown>>,
|
|
31
|
+
): Array<Record<string, unknown>> {
|
|
32
|
+
if (hops.length <= MAX_AGENT_PROVENANCE_HOPS) return hops;
|
|
33
|
+
return [hops[0]!, ...hops.slice(-(MAX_AGENT_PROVENANCE_HOPS - 1))];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function initiatorContextForStorage(
|
|
37
|
+
initiator: TurnInitiator,
|
|
38
|
+
context: TurnInitiatorContext = {},
|
|
39
|
+
): TurnInitiatorContext {
|
|
40
|
+
return initiator.label ? { ...context, label: initiator.label } : { ...context };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function initiatorFromStorage(
|
|
44
|
+
kind: string,
|
|
45
|
+
subjectId: string,
|
|
46
|
+
context: TurnInitiatorContext,
|
|
47
|
+
): TurnInitiator {
|
|
48
|
+
const label =
|
|
49
|
+
typeof context.label === "string" && context.label.length > 0 ? context.label : null;
|
|
50
|
+
return {
|
|
51
|
+
kind: kind === "subject" ? "subject" : "service",
|
|
52
|
+
subjectId,
|
|
53
|
+
...(label ? { label } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function initiatorColumns(value: FrozenTurnInitiator): {
|
|
58
|
+
initiatorKind: TurnInitiator["kind"];
|
|
59
|
+
initiatorSubjectId: string;
|
|
60
|
+
initiatorContext: TurnInitiatorContext;
|
|
61
|
+
} {
|
|
62
|
+
return {
|
|
63
|
+
initiatorKind: value.initiator.kind,
|
|
64
|
+
initiatorSubjectId: value.initiator.subjectId,
|
|
65
|
+
initiatorContext: initiatorContextForStorage(value.initiator, value.context),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function creatorColumns(value: FrozenTurnInitiator): {
|
|
70
|
+
createdByKind: TurnInitiator["kind"];
|
|
71
|
+
createdBySubjectId: string;
|
|
72
|
+
createdByContext: TurnInitiatorContext;
|
|
73
|
+
} {
|
|
74
|
+
return {
|
|
75
|
+
createdByKind: value.initiator.kind,
|
|
76
|
+
createdBySubjectId: value.initiator.subjectId,
|
|
77
|
+
createdByContext: initiatorContextForStorage(value.initiator, value.context),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validAgentHops(value: unknown): Array<Record<string, unknown>> {
|
|
82
|
+
if (!Array.isArray(value)) return [];
|
|
83
|
+
return value.filter(
|
|
84
|
+
(hop): hop is Record<string, unknown> =>
|
|
85
|
+
typeof hop === "object" && hop !== null && !Array.isArray(hop),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function frozenInitiatorForCommandActor(
|
|
90
|
+
db: Database,
|
|
91
|
+
workspaceId: string,
|
|
92
|
+
actor: SessionCommandActor,
|
|
93
|
+
subjectLabel?: string,
|
|
94
|
+
): Promise<FrozenTurnInitiator> {
|
|
95
|
+
if (actor.type === "service") {
|
|
96
|
+
return {
|
|
97
|
+
initiator: {
|
|
98
|
+
kind: "service",
|
|
99
|
+
subjectId: actor.subjectId,
|
|
100
|
+
...(actor.subjectLabel ? { label: actor.subjectLabel } : {}),
|
|
101
|
+
},
|
|
102
|
+
context: { ...(actor.context ?? {}) },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (actor.type !== "agent_attempt") {
|
|
106
|
+
return {
|
|
107
|
+
initiator: {
|
|
108
|
+
kind: "subject",
|
|
109
|
+
subjectId: actor.subjectId,
|
|
110
|
+
...(subjectLabel ? { label: subjectLabel } : {}),
|
|
111
|
+
},
|
|
112
|
+
context: {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const [turn] = await db
|
|
117
|
+
.select({
|
|
118
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
119
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
120
|
+
initiatorContext: schema.sessionTurns.initiatorContext,
|
|
121
|
+
})
|
|
122
|
+
.from(schema.sessionTurns)
|
|
123
|
+
.where(
|
|
124
|
+
and(
|
|
125
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
126
|
+
eq(schema.sessionTurns.sessionId, actor.sessionId),
|
|
127
|
+
eq(schema.sessionTurns.id, actor.turnId),
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
.limit(1);
|
|
131
|
+
if (!turn) {
|
|
132
|
+
throw new Error(`Agent initiator turn not found: ${actor.turnId}`);
|
|
133
|
+
}
|
|
134
|
+
const storedContext = turn.initiatorContext ?? {};
|
|
135
|
+
const inheritedHops = validAgentHops(storedContext.via);
|
|
136
|
+
const hops = [
|
|
137
|
+
...inheritedHops,
|
|
138
|
+
{
|
|
139
|
+
kind: "agent",
|
|
140
|
+
sessionId: actor.sessionId,
|
|
141
|
+
turnId: actor.turnId,
|
|
142
|
+
attemptId: actor.attemptId,
|
|
143
|
+
executionGeneration: actor.executionGeneration,
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
const clipped = clipAgentProvenanceHops(hops);
|
|
147
|
+
return {
|
|
148
|
+
initiator: initiatorFromStorage(turn.initiatorKind, turn.initiatorSubjectId, storedContext),
|
|
149
|
+
context: {
|
|
150
|
+
...storedContext,
|
|
151
|
+
via: clipped,
|
|
152
|
+
...(hops.length > clipped.length ? { viaTruncated: true } : {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
package/dist/chunk-7LDU7F5P.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// src/migrate.ts
|
|
2
|
-
import { readdir, readFile } from "fs/promises";
|
|
3
|
-
import { dirname, join } from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
import postgres from "postgres";
|
|
6
|
-
var DEFAULT_DATABASE_URL = "postgres://opengeni:opengeni@127.0.0.1:5432/opengeni";
|
|
7
|
-
var concurrentIndexDirective = /^-- opengeni:concurrent-index lock-timeout=(\d+(?:ms|s|min))$/;
|
|
8
|
-
function assertIdentifier(name, value) {
|
|
9
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
10
|
-
throw new Error(`${name} is not a valid Postgres identifier: ${value}`);
|
|
11
|
-
}
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
async function executeMigrationFile(sql, file, sqlText) {
|
|
15
|
-
const [firstLine = "", ...remainingLines] = sqlText.replaceAll("\r\n", "\n").split("\n");
|
|
16
|
-
const directive = concurrentIndexDirective.exec(firstLine.trim());
|
|
17
|
-
if (!directive) {
|
|
18
|
-
if (firstLine.trim().startsWith("-- opengeni:")) {
|
|
19
|
-
throw new Error(`Unsupported OpenGeni migration directive in ${file}`);
|
|
20
|
-
}
|
|
21
|
-
await sql.unsafe(sqlText);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const lockTimeout = directive[1];
|
|
25
|
-
const statement = remainingLines.join("\n").trim();
|
|
26
|
-
const withoutTrailingSemicolon = statement.endsWith(";") ? statement.slice(0, -1).trimEnd() : statement;
|
|
27
|
-
if (!/^CREATE\s+(?:UNIQUE\s+)?INDEX\s+CONCURRENTLY\b/is.test(withoutTrailingSemicolon) || withoutTrailingSemicolon.includes(";")) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`${file}: opengeni:concurrent-index requires exactly one CREATE [UNIQUE] INDEX CONCURRENTLY statement`
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
await sql`select set_config('lock_timeout', ${lockTimeout}, false)`;
|
|
33
|
-
try {
|
|
34
|
-
await sql.unsafe(statement);
|
|
35
|
-
} finally {
|
|
36
|
-
await sql`select set_config('lock_timeout', '0', false)`;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function migrate(databaseUrl = process.env.OPENGENI_MIGRATIONS_DATABASE_URL ?? process.env.OPENGENI_DATABASE_URL ?? DEFAULT_DATABASE_URL, schema = process.env.OPENGENI_DB_SCHEMA?.trim() || void 0) {
|
|
40
|
-
const migrationsDir = join(dirname(fileURLToPath(import.meta.url)), "../drizzle");
|
|
41
|
-
const files = (await readdir(migrationsDir)).filter((file) => file.endsWith(".sql")).sort();
|
|
42
|
-
const sql = postgres(databaseUrl, { max: 1 });
|
|
43
|
-
try {
|
|
44
|
-
await sql`SELECT pg_advisory_lock(727458)`;
|
|
45
|
-
if (schema) {
|
|
46
|
-
assertIdentifier("OPENGENI_DB_SCHEMA", schema);
|
|
47
|
-
await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
|
|
48
|
-
await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS "opengeni_private"`);
|
|
49
|
-
await sql.unsafe(`SET search_path = "${schema}", "opengeni_private", "public"`);
|
|
50
|
-
}
|
|
51
|
-
await sql.unsafe(
|
|
52
|
-
`CREATE TABLE IF NOT EXISTS "schema_migrations" ("name" text PRIMARY KEY, "applied_at" timestamptz NOT NULL DEFAULT now())`
|
|
53
|
-
);
|
|
54
|
-
const appliedRows = await sql`SELECT "name" FROM "schema_migrations"`;
|
|
55
|
-
const applied = new Set(appliedRows.map((row) => row.name));
|
|
56
|
-
for (const file of files) {
|
|
57
|
-
if (applied.has(file)) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const sqlText = await readFile(join(migrationsDir, file), "utf8");
|
|
61
|
-
await executeMigrationFile(sql, file, sqlText);
|
|
62
|
-
await sql`INSERT INTO "schema_migrations" ("name") VALUES (${file}) ON CONFLICT DO NOTHING`;
|
|
63
|
-
}
|
|
64
|
-
} finally {
|
|
65
|
-
await sql.end();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async function runMigrations(adminConnection, targetSchema) {
|
|
69
|
-
await migrate(adminConnection, targetSchema);
|
|
70
|
-
}
|
|
71
|
-
if (import.meta.main) {
|
|
72
|
-
await migrate();
|
|
73
|
-
console.log("Applied Drizzle SQL migrations.");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export {
|
|
77
|
-
migrate,
|
|
78
|
-
runMigrations
|
|
79
|
-
};
|
|
80
|
-
//# sourceMappingURL=chunk-7LDU7F5P.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport postgres from \"postgres\";\n\nconst DEFAULT_DATABASE_URL = \"postgres://opengeni:opengeni@127.0.0.1:5432/opengeni\";\nconst concurrentIndexDirective = /^-- opengeni:concurrent-index lock-timeout=(\\d+(?:ms|s|min))$/;\n\n/** A bare Postgres identifier (schema/role name) safe to interpolate into DDL. */\nfunction assertIdentifier(name: string, value: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {\n throw new Error(`${name} is not a valid Postgres identifier: ${value}`);\n }\n return value;\n}\n\n/**\n * Most migration files intentionally execute as one implicit transaction.\n * PostgreSQL forbids CREATE INDEX CONCURRENTLY there, so a migration may opt\n * into one narrowly validated transactionless statement with:\n *\n * -- opengeni:concurrent-index lock-timeout=5s\n * CREATE [UNIQUE] INDEX CONCURRENTLY ...;\n *\n * The directive is deliberately not a generic \"no transaction\" escape hatch:\n * only one concurrent-index statement is accepted, and lock acquisition is\n * always bounded. This keeps additive large-table indexes online without making\n * arbitrary partially-applied migration scripts possible.\n */\nasync function executeMigrationFile(\n sql: postgres.Sql,\n file: string,\n sqlText: string,\n): Promise<void> {\n const [firstLine = \"\", ...remainingLines] = sqlText.replaceAll(\"\\r\\n\", \"\\n\").split(\"\\n\");\n const directive = concurrentIndexDirective.exec(firstLine.trim());\n if (!directive) {\n if (firstLine.trim().startsWith(\"-- opengeni:\")) {\n throw new Error(`Unsupported OpenGeni migration directive in ${file}`);\n }\n await sql.unsafe(sqlText);\n return;\n }\n\n const lockTimeout = directive[1]!;\n const statement = remainingLines.join(\"\\n\").trim();\n const withoutTrailingSemicolon = statement.endsWith(\";\")\n ? statement.slice(0, -1).trimEnd()\n : statement;\n if (\n !/^CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+CONCURRENTLY\\b/is.test(withoutTrailingSemicolon) ||\n withoutTrailingSemicolon.includes(\";\")\n ) {\n throw new Error(\n `${file}: opengeni:concurrent-index requires exactly one CREATE [UNIQUE] INDEX CONCURRENTLY statement`,\n );\n }\n\n await sql`select set_config('lock_timeout', ${lockTimeout}, false)`;\n try {\n await sql.unsafe(statement);\n } finally {\n await sql`select set_config('lock_timeout', '0', false)`;\n }\n}\n\n/**\n * Apply the OpenGeni SQL migration chain.\n *\n * STANDALONE (default, unchanged): `migrate()` / `migrate(databaseUrl)` runs the\n * whole chain with NO search_path manipulation, so every unqualified\n * table/index/policy lands in the server default schema (`public`). This is the\n * byte-for-byte historical behavior — the migration test suite calls\n * `migrate(DB_URL)` and is unaffected.\n *\n * EMBEDDED (Step I, §7.8 runtime/SDK half): pass a `schema` (or set\n * `OPENGENI_DB_SCHEMA`). The migrate session then `CREATE SCHEMA IF NOT EXISTS`\n * for both `<schema>` and `opengeni_private`, and sets\n * `search_path = \"<schema>\", \"opengeni_private\", \"public\"`, so EVERY unqualified\n * DDL statement lands in the dedicated schema with NO per-statement SQL rewrite\n * (the SPIKE-1 F1 result). Two things make this work and stay idempotent:\n * 1. The policy-existence guards in the migration SQL use `current_schema()`\n * (not a hardcoded `'public'`) — so a re-run finds the policy it already\n * created in `<schema>` and DROP/CREATEs idempotently instead of failing\n * with \"policy already exists\". (This guard substitution is the migrate-\n * time enabler for the runtime search_path approach; without it the SDK\n * entry point silently fails on re-run — the Fork-6 hazard.)\n * 2. `public` stays LAST on the path so `gen_random_uuid()` (pgcrypto) and the\n * `vector` type — both installed into `public` by 0000 — still resolve. The\n * `opengeni_private.*` helpers are always called with an absolute prefix.\n *\n * `OPENGENI_DB_SCHEMA` defaults UNSET → `public` → standalone, so the default\n * binding never regresses.\n */\nexport async function migrate(\n databaseUrl = process.env.OPENGENI_MIGRATIONS_DATABASE_URL ??\n process.env.OPENGENI_DATABASE_URL ??\n DEFAULT_DATABASE_URL,\n schema: string | undefined = process.env.OPENGENI_DB_SCHEMA?.trim() || undefined,\n): Promise<void> {\n const migrationsDir = join(dirname(fileURLToPath(import.meta.url)), \"../drizzle\");\n const files = (await readdir(migrationsDir)).filter((file) => file.endsWith(\".sql\")).sort();\n const sql = postgres(databaseUrl, { max: 1 });\n try {\n // Serialize concurrent migrate() runs; the session-level lock is released\n // when the connection closes.\n await sql`SELECT pg_advisory_lock(727458)`;\n if (schema) {\n assertIdentifier(\"OPENGENI_DB_SCHEMA\", schema);\n await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS \"${schema}\"`);\n // opengeni_private is also created by 0001 with an absolute prefix, but the\n // session search_path must already resolve it for the policy predicates\n // and the SECURITY DEFINER functions that inherit the caller's path.\n await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS \"opengeni_private\"`);\n await sql.unsafe(`SET search_path = \"${schema}\", \"opengeni_private\", \"public\"`);\n }\n await sql.unsafe(\n `CREATE TABLE IF NOT EXISTS \"schema_migrations\" (\"name\" text PRIMARY KEY, \"applied_at\" timestamptz NOT NULL DEFAULT now())`,\n );\n const appliedRows = await sql`SELECT \"name\" FROM \"schema_migrations\"`;\n const applied = new Set(appliedRows.map((row) => row.name as string));\n for (const file of files) {\n if (applied.has(file)) {\n continue;\n }\n const sqlText = await readFile(join(migrationsDir, file), \"utf8\");\n await executeMigrationFile(sql, file, sqlText);\n await sql`INSERT INTO \"schema_migrations\" (\"name\") VALUES (${file}) ON CONFLICT DO NOTHING`;\n }\n } finally {\n await sql.end();\n }\n}\n\n/**\n * SDK entry point (Step I): run the migration chain over a host-supplied admin\n * connection string against an explicit target schema. This is the embedded\n * topology's named entry — a host calls `runMigrations(adminConnection,\n * targetSchema)` from its own provisioning code instead of relying on env vars.\n * `targetSchema` undefined → `public` → standalone behavior. Thin wrapper over\n * `migrate` so there is one migration engine.\n */\nexport async function runMigrations(adminConnection: string, targetSchema?: string): Promise<void> {\n await migrate(adminConnection, targetSchema);\n}\n\nif (import.meta.main) {\n await migrate();\n console.log(\"Applied Drizzle SQL migrations.\");\n}\n"],"mappings":";AAAA,SAAS,SAAS,gBAAgB;AAClC,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAC9B,OAAO,cAAc;AAErB,IAAM,uBAAuB;AAC7B,IAAM,2BAA2B;AAGjC,SAAS,iBAAiB,MAAc,OAAuB;AAC7D,MAAI,CAAC,2BAA2B,KAAK,KAAK,GAAG;AAC3C,UAAM,IAAI,MAAM,GAAG,IAAI,wCAAwC,KAAK,EAAE;AAAA,EACxE;AACA,SAAO;AACT;AAeA,eAAe,qBACb,KACA,MACA,SACe;AACf,QAAM,CAAC,YAAY,IAAI,GAAG,cAAc,IAAI,QAAQ,WAAW,QAAQ,IAAI,EAAE,MAAM,IAAI;AACvF,QAAM,YAAY,yBAAyB,KAAK,UAAU,KAAK,CAAC;AAChE,MAAI,CAAC,WAAW;AACd,QAAI,UAAU,KAAK,EAAE,WAAW,cAAc,GAAG;AAC/C,YAAM,IAAI,MAAM,+CAA+C,IAAI,EAAE;AAAA,IACvE;AACA,UAAM,IAAI,OAAO,OAAO;AACxB;AAAA,EACF;AAEA,QAAM,cAAc,UAAU,CAAC;AAC/B,QAAM,YAAY,eAAe,KAAK,IAAI,EAAE,KAAK;AACjD,QAAM,2BAA2B,UAAU,SAAS,GAAG,IACnD,UAAU,MAAM,GAAG,EAAE,EAAE,QAAQ,IAC/B;AACJ,MACE,CAAC,mDAAmD,KAAK,wBAAwB,KACjF,yBAAyB,SAAS,GAAG,GACrC;AACA,UAAM,IAAI;AAAA,MACR,GAAG,IAAI;AAAA,IACT;AAAA,EACF;AAEA,QAAM,wCAAwC,WAAW;AACzD,MAAI;AACF,UAAM,IAAI,OAAO,SAAS;AAAA,EAC5B,UAAE;AACA,UAAM;AAAA,EACR;AACF;AA8BA,eAAsB,QACpB,cAAc,QAAQ,IAAI,oCACxB,QAAQ,IAAI,yBACZ,sBACF,SAA6B,QAAQ,IAAI,oBAAoB,KAAK,KAAK,QACxD;AACf,QAAM,gBAAgB,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,YAAY;AAChF,QAAM,SAAS,MAAM,QAAQ,aAAa,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,MAAM,CAAC,EAAE,KAAK;AAC1F,QAAM,MAAM,SAAS,aAAa,EAAE,KAAK,EAAE,CAAC;AAC5C,MAAI;AAGF,UAAM;AACN,QAAI,QAAQ;AACV,uBAAiB,sBAAsB,MAAM;AAC7C,YAAM,IAAI,OAAO,gCAAgC,MAAM,GAAG;AAI1D,YAAM,IAAI,OAAO,gDAAgD;AACjE,YAAM,IAAI,OAAO,sBAAsB,MAAM,iCAAiC;AAAA,IAChF;AACA,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AACA,UAAM,cAAc,MAAM;AAC1B,UAAM,UAAU,IAAI,IAAI,YAAY,IAAI,CAAC,QAAQ,IAAI,IAAc,CAAC;AACpE,eAAW,QAAQ,OAAO;AACxB,UAAI,QAAQ,IAAI,IAAI,GAAG;AACrB;AAAA,MACF;AACA,YAAM,UAAU,MAAM,SAAS,KAAK,eAAe,IAAI,GAAG,MAAM;AAChE,YAAM,qBAAqB,KAAK,MAAM,OAAO;AAC7C,YAAM,uDAAuD,IAAI;AAAA,IACnE;AAAA,EACF,UAAE;AACA,UAAM,IAAI,IAAI;AAAA,EAChB;AACF;AAUA,eAAsB,cAAc,iBAAyB,cAAsC;AACjG,QAAM,QAAQ,iBAAiB,YAAY;AAC7C;AAEA,IAAI,YAAY,MAAM;AACpB,QAAM,QAAQ;AACd,UAAQ,IAAI,iCAAiC;AAC/C;","names":[]}
|