@superblocksteam/shared 0.9614.3 → 0.9615.1
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/database-lifecycle/appDatabaseCompatibility.d.ts +5 -3
- package/dist/database-lifecycle/appDatabaseCompatibility.d.ts.map +1 -1
- package/dist/database-lifecycle/appDatabaseCompatibility.js.map +1 -1
- package/dist/database-lifecycle/appDatabaseImport.d.ts +70 -0
- package/dist/database-lifecycle/appDatabaseImport.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseImport.js +111 -0
- package/dist/database-lifecycle/appDatabaseImport.js.map +1 -0
- package/dist/database-lifecycle/appDatabaseImport.test.d.ts +2 -0
- package/dist/database-lifecycle/appDatabaseImport.test.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseImport.test.js +145 -0
- package/dist/database-lifecycle/appDatabaseImport.test.js.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.d.ts +79 -0
- package/dist/database-lifecycle/appDatabaseStatus.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.js +28 -0
- package/dist/database-lifecycle/appDatabaseStatus.js.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.d.ts +2 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.d.ts.map +1 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.js +88 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.js.map +1 -0
- package/dist/database-lifecycle/dataImport.d.ts +53 -0
- package/dist/database-lifecycle/dataImport.d.ts.map +1 -0
- package/dist/database-lifecycle/dataImport.js +173 -0
- package/dist/database-lifecycle/dataImport.js.map +1 -0
- package/dist/database-lifecycle/dataImport.test.d.ts +2 -0
- package/dist/database-lifecycle/dataImport.test.d.ts.map +1 -0
- package/dist/database-lifecycle/dataImport.test.js +84 -0
- package/dist/database-lifecycle/dataImport.test.js.map +1 -0
- package/dist/database-lifecycle/index.d.ts +101 -5
- package/dist/database-lifecycle/index.d.ts.map +1 -1
- package/dist/database-lifecycle/index.js +55 -6
- package/dist/database-lifecycle/index.js.map +1 -1
- package/dist/database-lifecycle/index.test.js +18 -6
- package/dist/database-lifecycle/index.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/templates/workflow.d.ts.map +1 -1
- package/dist/plugins/templates/workflow.js +1 -3
- package/dist/plugins/templates/workflow.js.map +1 -1
- package/dist/socket/protocol.d.ts +19 -2
- package/dist/socket/protocol.d.ts.map +1 -1
- package/dist/socket/protocol.js.map +1 -1
- package/dist/types/ai/index.d.ts +39 -0
- package/dist/types/ai/index.d.ts.map +1 -1
- package/dist/types/ai/index.js +8 -1
- package/dist/types/ai/index.js.map +1 -1
- package/dist/types/billing/usageSource.d.ts +1 -0
- package/dist/types/billing/usageSource.d.ts.map +1 -1
- package/dist/types/billing/usageSource.js +19 -1
- package/dist/types/billing/usageSource.js.map +1 -1
- package/dist/types/event/index.d.ts +6 -1
- package/dist/types/event/index.d.ts.map +1 -1
- package/dist/types/event/index.js +5 -0
- package/dist/types/event/index.js.map +1 -1
- package/dist/types/policyGate/index.d.ts +11 -0
- package/dist/types/policyGate/index.d.ts.map +1 -1
- package/dist/types/policyGate/index.js +9 -0
- package/dist/types/policyGate/index.js.map +1 -1
- package/dist/utils/attachment-delivery.js +1 -1
- package/dist/utils/attachment-delivery.js.map +1 -1
- package/dist/utils/attachment-delivery.test.js +11 -0
- package/dist/utils/attachment-delivery.test.js.map +1 -1
- package/dist/utils/attachment-upload.d.ts +15 -0
- package/dist/utils/attachment-upload.d.ts.map +1 -1
- package/dist/utils/attachment-upload.js +44 -2
- package/dist/utils/attachment-upload.js.map +1 -1
- package/dist/utils/attachment-upload.test.js +37 -1
- package/dist/utils/attachment-upload.test.js.map +1 -1
- package/dist/utils/attachment-validation.d.ts +18 -1
- package/dist/utils/attachment-validation.d.ts.map +1 -1
- package/dist/utils/attachment-validation.js +38 -5
- package/dist/utils/attachment-validation.js.map +1 -1
- package/dist/utils/attachment-validation.test.js +22 -3
- package/dist/utils/attachment-validation.test.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseCompatibility.d.ts +5 -3
- package/dist-esm/database-lifecycle/appDatabaseCompatibility.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseCompatibility.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts +70 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.js +104 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.js.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js +143 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts +79 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.js +23 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.js.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.js +86 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.js.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.d.ts +53 -0
- package/dist-esm/database-lifecycle/dataImport.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.js +165 -0
- package/dist-esm/database-lifecycle/dataImport.js.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/dataImport.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/dataImport.test.js +82 -0
- package/dist-esm/database-lifecycle/dataImport.test.js.map +1 -0
- package/dist-esm/database-lifecycle/index.d.ts +101 -5
- package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/index.js +36 -5
- package/dist-esm/database-lifecycle/index.js.map +1 -1
- package/dist-esm/database-lifecycle/index.test.js +19 -7
- package/dist-esm/database-lifecycle/index.test.js.map +1 -1
- package/dist-esm/index.d.ts +1 -0
- package/dist-esm/index.d.ts.map +1 -1
- package/dist-esm/index.js +1 -0
- package/dist-esm/index.js.map +1 -1
- package/dist-esm/plugins/templates/workflow.d.ts.map +1 -1
- package/dist-esm/plugins/templates/workflow.js +1 -3
- package/dist-esm/plugins/templates/workflow.js.map +1 -1
- package/dist-esm/socket/protocol.d.ts +19 -2
- package/dist-esm/socket/protocol.d.ts.map +1 -1
- package/dist-esm/socket/protocol.js.map +1 -1
- package/dist-esm/types/ai/index.d.ts +39 -0
- package/dist-esm/types/ai/index.d.ts.map +1 -1
- package/dist-esm/types/ai/index.js +6 -0
- package/dist-esm/types/ai/index.js.map +1 -1
- package/dist-esm/types/billing/usageSource.d.ts +1 -0
- package/dist-esm/types/billing/usageSource.d.ts.map +1 -1
- package/dist-esm/types/billing/usageSource.js +18 -0
- package/dist-esm/types/billing/usageSource.js.map +1 -1
- package/dist-esm/types/event/index.d.ts +6 -1
- package/dist-esm/types/event/index.d.ts.map +1 -1
- package/dist-esm/types/event/index.js +5 -0
- package/dist-esm/types/event/index.js.map +1 -1
- package/dist-esm/types/policyGate/index.d.ts +11 -0
- package/dist-esm/types/policyGate/index.d.ts.map +1 -1
- package/dist-esm/types/policyGate/index.js +8 -0
- package/dist-esm/types/policyGate/index.js.map +1 -1
- package/dist-esm/utils/attachment-delivery.js +2 -2
- package/dist-esm/utils/attachment-delivery.js.map +1 -1
- package/dist-esm/utils/attachment-delivery.test.js +11 -0
- package/dist-esm/utils/attachment-delivery.test.js.map +1 -1
- package/dist-esm/utils/attachment-upload.d.ts +15 -0
- package/dist-esm/utils/attachment-upload.d.ts.map +1 -1
- package/dist-esm/utils/attachment-upload.js +41 -1
- package/dist-esm/utils/attachment-upload.js.map +1 -1
- package/dist-esm/utils/attachment-upload.test.js +38 -2
- package/dist-esm/utils/attachment-upload.test.js.map +1 -1
- package/dist-esm/utils/attachment-validation.d.ts +18 -1
- package/dist-esm/utils/attachment-validation.d.ts.map +1 -1
- package/dist-esm/utils/attachment-validation.js +38 -5
- package/dist-esm/utils/attachment-validation.js.map +1 -1
- package/dist-esm/utils/attachment-validation.test.js +23 -4
- package/dist-esm/utils/attachment-validation.test.js.map +1 -1
- package/package.json +1 -1
- package/src/database-lifecycle/appDatabaseCompatibility.ts +6 -3
- package/src/database-lifecycle/appDatabaseImport.test.ts +185 -0
- package/src/database-lifecycle/appDatabaseImport.ts +205 -0
- package/src/database-lifecycle/appDatabaseStatus.test.ts +105 -0
- package/src/database-lifecycle/appDatabaseStatus.ts +95 -0
- package/src/database-lifecycle/dataImport.test.ts +95 -0
- package/src/database-lifecycle/dataImport.ts +244 -0
- package/src/database-lifecycle/index.test.ts +20 -6
- package/src/database-lifecycle/index.ts +203 -7
- package/src/index.ts +1 -0
- package/src/plugins/templates/workflow.ts +1 -3
- package/src/socket/protocol.ts +14 -2
- package/src/types/ai/index.ts +39 -0
- package/src/types/billing/usageSource.ts +20 -0
- package/src/types/event/index.ts +6 -1
- package/src/types/policyGate/index.ts +14 -0
- package/src/utils/attachment-delivery.test.ts +14 -0
- package/src/utils/attachment-delivery.ts +2 -2
- package/src/utils/attachment-upload.test.ts +48 -1
- package/src/utils/attachment-upload.ts +48 -1
- package/src/utils/attachment-validation.test.ts +35 -3
- package/src/utils/attachment-validation.ts +52 -6
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
export const DATA_IMPORT_KINDS = ['csv', 'pg_dump_custom'] as const;
|
|
2
|
+
// CHECK union for every kind. Prefix/schema success is `schema_validated`.
|
|
3
|
+
// `ready` stays available for CSV/JSON "validated, waiting to apply".
|
|
4
|
+
// Only `completed`, `failed`, and `cancelled` release the concurrency slot.
|
|
5
|
+
export const DATA_IMPORT_STATES = [
|
|
6
|
+
'requested',
|
|
7
|
+
'preparing_upload',
|
|
8
|
+
'preparing_prefix_upload',
|
|
9
|
+
'awaiting_upload',
|
|
10
|
+
'awaiting_prefix_upload',
|
|
11
|
+
'uploading',
|
|
12
|
+
'upload_completed',
|
|
13
|
+
'validating',
|
|
14
|
+
'validating_schema',
|
|
15
|
+
'schema_validated',
|
|
16
|
+
'ready',
|
|
17
|
+
'importing',
|
|
18
|
+
'completed',
|
|
19
|
+
'failed',
|
|
20
|
+
'cancelled'
|
|
21
|
+
] as const;
|
|
22
|
+
const DATA_IMPORT_TERMINAL_STATES = ['completed', 'failed', 'cancelled'] as const;
|
|
23
|
+
export const DATA_IMPORT_ACTIVE_STATES = DATA_IMPORT_STATES.filter(
|
|
24
|
+
(state) => !(DATA_IMPORT_TERMINAL_STATES as readonly string[]).includes(state)
|
|
25
|
+
);
|
|
26
|
+
export const DATA_IMPORT_TARGET_TYPES = ['app_database'] as const;
|
|
27
|
+
|
|
28
|
+
export type DataImportKind = (typeof DATA_IMPORT_KINDS)[number];
|
|
29
|
+
export type DataImportState = (typeof DATA_IMPORT_STATES)[number];
|
|
30
|
+
export type DataImportTargetType = (typeof DATA_IMPORT_TARGET_TYPES)[number];
|
|
31
|
+
|
|
32
|
+
export type DataImportTargetBase = {
|
|
33
|
+
type: DataImportTargetType;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type AppDatabaseDataImportTarget = DataImportTargetBase & {
|
|
37
|
+
applicationId: string;
|
|
38
|
+
bindingId: string;
|
|
39
|
+
profile: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// The persistence layer stores one target descriptor instead of destination-
|
|
43
|
+
// specific columns. Add future destinations as new discriminated variants.
|
|
44
|
+
export type DataImportTarget = AppDatabaseDataImportTarget;
|
|
45
|
+
|
|
46
|
+
export type DataImportUploadMetadata = {
|
|
47
|
+
completedPartNumbers?: number[];
|
|
48
|
+
filename: string;
|
|
49
|
+
prefixExpiresAt?: number;
|
|
50
|
+
objectChecksumSha256?: string;
|
|
51
|
+
objectKey?: string;
|
|
52
|
+
objectSizeBytes?: string;
|
|
53
|
+
partCount?: number;
|
|
54
|
+
partSizeBytes?: number;
|
|
55
|
+
uploadId?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Kind-specific facts kept in one jsonb column. Discriminate by kind so CSV
|
|
59
|
+
// table/mode cannot be stored in dump prefix fields. File contents, SQL,
|
|
60
|
+
// signed URLs, credentials, ETags, and row samples stay out.
|
|
61
|
+
export type PgDumpDataImportMetadata = {
|
|
62
|
+
kind: 'pg_dump_custom';
|
|
63
|
+
currentPrefixSizeBytes?: number;
|
|
64
|
+
postgresMajorVersion?: number;
|
|
65
|
+
prefixChecksumSha256?: string;
|
|
66
|
+
prefixObjectKey?: string;
|
|
67
|
+
requestedPrefixSizeBytes?: number;
|
|
68
|
+
tableNames?: string[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type CsvDataImportMetadata = {
|
|
72
|
+
kind: 'csv';
|
|
73
|
+
mode: 'append' | 'replace';
|
|
74
|
+
table: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type DataImportMetadata = PgDumpDataImportMetadata | CsvDataImportMetadata;
|
|
78
|
+
|
|
79
|
+
// Apply/terminal counts. Decimal strings, not JSON numbers, so values above
|
|
80
|
+
// Number.MAX_SAFE_INTEGER do not round. Writers replace this object whole.
|
|
81
|
+
export type DataImportOutcome = {
|
|
82
|
+
rowsDeleted?: string;
|
|
83
|
+
rowsInserted?: string;
|
|
84
|
+
rowsRead?: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const DATA_IMPORT_OUTCOME_KEYS = ['rowsDeleted', 'rowsInserted', 'rowsRead'] as const;
|
|
88
|
+
|
|
89
|
+
// Kind decides the grain. Callers do not supply this string.
|
|
90
|
+
// pg_dump_custom: one in-flight dump per binding.
|
|
91
|
+
// csv: one in-flight import per canonical schema.table on that binding.
|
|
92
|
+
export function mintDataImportConcurrencyKey(metadata: DataImportMetadata, target: AppDatabaseDataImportTarget): string {
|
|
93
|
+
if (metadata.kind === 'pg_dump_custom') {
|
|
94
|
+
return `pg_dump_custom:${target.bindingId}`;
|
|
95
|
+
}
|
|
96
|
+
return `csv:${target.bindingId}:${metadata.table}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseDataImportTarget(value: unknown): DataImportTarget | undefined {
|
|
100
|
+
if (
|
|
101
|
+
!isUnknownRecord(value) ||
|
|
102
|
+
value.type !== 'app_database' ||
|
|
103
|
+
!isNonEmptyString(value.applicationId) ||
|
|
104
|
+
!isNonEmptyString(value.bindingId) ||
|
|
105
|
+
!isNonEmptyString(value.profile)
|
|
106
|
+
) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
applicationId: value.applicationId,
|
|
111
|
+
bindingId: value.bindingId,
|
|
112
|
+
profile: value.profile,
|
|
113
|
+
type: value.type
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseDataImportMetadata(kind: DataImportKind, value: unknown): DataImportMetadata | undefined {
|
|
118
|
+
if (!isUnknownRecord(value) || value.kind !== kind) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (kind === 'pg_dump_custom') {
|
|
122
|
+
return parsePgDumpMetadata(value);
|
|
123
|
+
}
|
|
124
|
+
const table = canonicalCsvTable(value.table);
|
|
125
|
+
if (table === undefined || (value.mode !== 'append' && value.mode !== 'replace')) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const allowed = new Set(['kind', 'mode', 'table']);
|
|
129
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return { kind: 'csv', mode: value.mode, table };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseDataImportOutcome(value: unknown): DataImportOutcome | undefined {
|
|
136
|
+
if (!isUnknownRecord(value)) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
if (Object.keys(value).some((key) => !DATA_IMPORT_OUTCOME_KEYS.includes(key as (typeof DATA_IMPORT_OUTCOME_KEYS)[number]))) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const outcome: DataImportOutcome = {};
|
|
143
|
+
for (const key of DATA_IMPORT_OUTCOME_KEYS) {
|
|
144
|
+
const count = value[key];
|
|
145
|
+
if (count === undefined) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!isDecimalCount(count)) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
outcome[key] = count;
|
|
152
|
+
}
|
|
153
|
+
return outcome;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function sqlStringList(values: readonly string[]): string {
|
|
157
|
+
return values.map((value) => `'${value}'`).join(', ');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
161
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// One unquoted lowercase schema.table. Each part is at most 63 bytes, which
|
|
165
|
+
// is PostgreSQL's identifier limit. A longer spelling would truncate to the
|
|
166
|
+
// same relation as another accepted key.
|
|
167
|
+
const CANONICAL_CSV_TABLE = /^[a-z_][a-z0-9_]{0,62}\.[a-z_][a-z0-9_]{0,62}$/;
|
|
168
|
+
|
|
169
|
+
function canonicalCsvTable(value: unknown): string | undefined {
|
|
170
|
+
if (typeof value !== 'string') {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const table = value.trim();
|
|
174
|
+
if (!CANONICAL_CSV_TABLE.test(table)) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return table;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parsePgDumpMetadata(value: Record<string, unknown>): PgDumpDataImportMetadata | undefined {
|
|
181
|
+
const metadata: PgDumpDataImportMetadata = { kind: 'pg_dump_custom' };
|
|
182
|
+
if (!assignOptionalString(metadata, 'prefixChecksumSha256', value.prefixChecksumSha256)) return undefined;
|
|
183
|
+
if (!assignOptionalString(metadata, 'prefixObjectKey', value.prefixObjectKey)) return undefined;
|
|
184
|
+
if (!assignOptionalInteger(metadata, 'currentPrefixSizeBytes', value.currentPrefixSizeBytes)) return undefined;
|
|
185
|
+
if (!assignOptionalInteger(metadata, 'postgresMajorVersion', value.postgresMajorVersion)) return undefined;
|
|
186
|
+
if (!assignOptionalInteger(metadata, 'requestedPrefixSizeBytes', value.requestedPrefixSizeBytes)) return undefined;
|
|
187
|
+
if (value.tableNames !== undefined) {
|
|
188
|
+
if (!Array.isArray(value.tableNames) || !value.tableNames.every(isNonEmptyString)) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
metadata.tableNames = value.tableNames;
|
|
192
|
+
}
|
|
193
|
+
const allowed = new Set([
|
|
194
|
+
'kind',
|
|
195
|
+
'currentPrefixSizeBytes',
|
|
196
|
+
'postgresMajorVersion',
|
|
197
|
+
'prefixChecksumSha256',
|
|
198
|
+
'prefixObjectKey',
|
|
199
|
+
'requestedPrefixSizeBytes',
|
|
200
|
+
'tableNames'
|
|
201
|
+
]);
|
|
202
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return metadata;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assignOptionalString<Key extends 'prefixChecksumSha256' | 'prefixObjectKey'>(
|
|
209
|
+
metadata: PgDumpDataImportMetadata,
|
|
210
|
+
key: Key,
|
|
211
|
+
value: unknown
|
|
212
|
+
): boolean {
|
|
213
|
+
if (value === undefined) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (!isNonEmptyString(value)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
metadata[key] = value;
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function assignOptionalInteger(
|
|
224
|
+
metadata: PgDumpDataImportMetadata,
|
|
225
|
+
key: 'currentPrefixSizeBytes' | 'postgresMajorVersion' | 'requestedPrefixSizeBytes',
|
|
226
|
+
value: unknown
|
|
227
|
+
): boolean {
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
metadata[key] = value;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isDecimalCount(value: unknown): value is string {
|
|
239
|
+
return typeof value === 'string' && /^(0|[1-9][0-9]*)$/.test(value);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
243
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
244
|
+
}
|
|
@@ -3,6 +3,7 @@ import { describe, expect, test } from 'vitest';
|
|
|
3
3
|
import { ApiTriggerType } from '../types/api/index.js';
|
|
4
4
|
import {
|
|
5
5
|
DATABASE_LIFECYCLE_MANAGED_BY,
|
|
6
|
+
DATABASE_LIFECYCLE_OPERATIONS,
|
|
6
7
|
LIFECYCLE_OPERATIONS,
|
|
7
8
|
computeBindingKey,
|
|
8
9
|
computeDesiredSpecHash,
|
|
@@ -20,17 +21,30 @@ import {
|
|
|
20
21
|
} from './index.js';
|
|
21
22
|
|
|
22
23
|
describe('database lifecycle contracts', () => {
|
|
23
|
-
|
|
24
|
-
// check treats enum label order as part of the contract, so this array must
|
|
25
|
-
// match the live `lifecycle_operation_enum` order rather than alphabetical
|
|
26
|
-
// or semantic grouping.
|
|
27
|
-
test('lists lifecycle operations in Postgres enum order', () => {
|
|
24
|
+
test('lists persisted operations first and appends import operations', () => {
|
|
28
25
|
expect(LIFECYCLE_OPERATIONS).toEqual([
|
|
29
26
|
'ensure_database',
|
|
30
27
|
'migrate_schema',
|
|
31
28
|
'retire_database',
|
|
32
29
|
'ensure_physical_database_instance',
|
|
33
|
-
'retire_physical_database_instance'
|
|
30
|
+
'retire_physical_database_instance',
|
|
31
|
+
'verify_database_infra_absence',
|
|
32
|
+
'prepare_import_upload',
|
|
33
|
+
'complete_import_upload',
|
|
34
|
+
'prepare_import_prefix_upload',
|
|
35
|
+
'inspect_import_prefix',
|
|
36
|
+
'describe_schema'
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('keeps import operations out of the default agent operation set', () => {
|
|
41
|
+
expect(DATABASE_LIFECYCLE_OPERATIONS).toEqual([
|
|
42
|
+
'ensure_database',
|
|
43
|
+
'migrate_schema',
|
|
44
|
+
'retire_database',
|
|
45
|
+
'ensure_physical_database_instance',
|
|
46
|
+
'retire_physical_database_instance',
|
|
47
|
+
'verify_database_infra_absence'
|
|
34
48
|
]);
|
|
35
49
|
});
|
|
36
50
|
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { sha256Base64 } from '../signing/hashing.js';
|
|
2
2
|
import { ApiTriggerType } from '../types/api/index.js';
|
|
3
|
+
import {
|
|
4
|
+
APP_DATABASE_IMPORT_OPERATIONS,
|
|
5
|
+
type AppDatabaseImportCallbackResult,
|
|
6
|
+
type AppDatabaseImportDispatchPayloadEnvelope,
|
|
7
|
+
type AppDatabaseImportErrorCode,
|
|
8
|
+
type AppDatabaseImportLifecycleOperation,
|
|
9
|
+
type InspectImportPrefixDispatchPayloadEnvelope,
|
|
10
|
+
type PrepareImportPrefixUploadDispatchPayloadEnvelope
|
|
11
|
+
} from './appDatabaseImport.js';
|
|
3
12
|
import type { DatabaseEngine } from './capacityPolicy.js';
|
|
13
|
+
import {
|
|
14
|
+
DATA_IMPORT_KINDS,
|
|
15
|
+
DATA_IMPORT_TARGET_TYPES,
|
|
16
|
+
type DataImportKind,
|
|
17
|
+
type DataImportTargetBase,
|
|
18
|
+
type DataImportTargetType
|
|
19
|
+
} from './dataImport.js';
|
|
4
20
|
|
|
5
21
|
export const LIFECYCLE_TERMINAL_STATES = ['ready', 'failed', 'cancelled'] as const;
|
|
6
22
|
export const LIFECYCLE_NON_TERMINAL_STATES = ['pending', 'provisioning', 'migrating', 'retiring'] as const;
|
|
@@ -13,16 +29,24 @@ export const LIFECYCLE_MIGRATION_STATES = ['pending', 'migrated', 'failed'] as c
|
|
|
13
29
|
// are claimable pool-level work (wizard / eager provisioning, admin
|
|
14
30
|
// decommission). Neither is bound to a database_binding row; the claim payload
|
|
15
31
|
// carries provisioningId or the instance id instead of binding identity.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
32
|
+
// Persisted database lifecycle operations come first in Postgres enum order.
|
|
33
|
+
// Import operations are appended. Generic upload operations stay out of the
|
|
34
|
+
// default agent set; prefix operations are stored by the data-import migration
|
|
35
|
+
// but are not part of that default set either.
|
|
19
36
|
export const LIFECYCLE_OPERATIONS = [
|
|
20
37
|
'ensure_database',
|
|
21
38
|
'migrate_schema',
|
|
22
39
|
'retire_database',
|
|
23
40
|
'ensure_physical_database_instance',
|
|
24
|
-
'retire_physical_database_instance'
|
|
41
|
+
'retire_physical_database_instance',
|
|
42
|
+
'verify_database_infra_absence',
|
|
43
|
+
'prepare_import_upload',
|
|
44
|
+
'complete_import_upload',
|
|
45
|
+
'prepare_import_prefix_upload',
|
|
46
|
+
'inspect_import_prefix',
|
|
47
|
+
'describe_schema'
|
|
25
48
|
] as const;
|
|
49
|
+
export const GENERIC_IMPORT_OPERATIONS = ['prepare_import_upload', 'complete_import_upload'] as const;
|
|
26
50
|
// The pool-scoped operations, which carry an organization directly and have no
|
|
27
51
|
// binding, desired spec, or desired spec hash. `CK_lifecycle_request_shape`
|
|
28
52
|
// enforces exactly this split at the database level.
|
|
@@ -55,6 +79,49 @@ export const DATABASE_LIFECYCLE_TAG_OPERATIONS = 'databaseLifecycle:operations';
|
|
|
55
79
|
export const DATABASE_LIFECYCLE_TAG_ENGINES = 'databaseLifecycle:engines';
|
|
56
80
|
export const DATABASE_LIFECYCLE_TAG_CAPABILITIES = 'databaseLifecycle:capabilities';
|
|
57
81
|
export const DATABASE_LIFECYCLE_TAG_PHYSICAL_CONFIG_FINGERPRINT = 'databaseLifecycle:physicalConfigFingerprint';
|
|
82
|
+
export {
|
|
83
|
+
APP_DATABASE_IMPORT_ERROR_CODES,
|
|
84
|
+
APP_DATABASE_IMPORT_OPERATIONS,
|
|
85
|
+
APP_DATABASE_IMPORT_STATES,
|
|
86
|
+
isAppDatabaseImportCallbackResult,
|
|
87
|
+
isAppDatabaseImportDispatchPayload,
|
|
88
|
+
isAppDatabaseImportLifecycleOperation,
|
|
89
|
+
parseAppDatabaseImportValidationMetadata,
|
|
90
|
+
type AppDatabaseImportCallbackResult,
|
|
91
|
+
type AppDatabaseImportDispatchDetails,
|
|
92
|
+
type AppDatabaseImportDispatchPayloadEnvelope,
|
|
93
|
+
type AppDatabaseImportErrorCode,
|
|
94
|
+
type AppDatabaseImportKind,
|
|
95
|
+
type AppDatabaseImportLifecycleOperation,
|
|
96
|
+
type AppDatabaseImportState,
|
|
97
|
+
type AppDatabaseImportValidationMetadata,
|
|
98
|
+
type InspectImportPrefixCallbackResult,
|
|
99
|
+
type InspectImportPrefixDispatchPayloadEnvelope,
|
|
100
|
+
type PrepareImportPrefixUploadCallbackResult,
|
|
101
|
+
type PrepareImportPrefixUploadDispatchPayloadEnvelope
|
|
102
|
+
} from './appDatabaseImport.js';
|
|
103
|
+
export {
|
|
104
|
+
DATA_IMPORT_ACTIVE_STATES,
|
|
105
|
+
DATA_IMPORT_KINDS,
|
|
106
|
+
DATA_IMPORT_STATES,
|
|
107
|
+
DATA_IMPORT_TARGET_TYPES,
|
|
108
|
+
mintDataImportConcurrencyKey,
|
|
109
|
+
parseDataImportMetadata,
|
|
110
|
+
parseDataImportOutcome,
|
|
111
|
+
parseDataImportTarget,
|
|
112
|
+
sqlStringList,
|
|
113
|
+
type AppDatabaseDataImportTarget,
|
|
114
|
+
type CsvDataImportMetadata,
|
|
115
|
+
type DataImportKind,
|
|
116
|
+
type DataImportTargetBase,
|
|
117
|
+
type DataImportMetadata,
|
|
118
|
+
type DataImportOutcome,
|
|
119
|
+
type DataImportState,
|
|
120
|
+
type PgDumpDataImportMetadata,
|
|
121
|
+
type DataImportTarget,
|
|
122
|
+
type DataImportTargetType,
|
|
123
|
+
type DataImportUploadMetadata
|
|
124
|
+
} from './dataImport.js';
|
|
58
125
|
export {
|
|
59
126
|
CapacityPolicyValidationError,
|
|
60
127
|
DATABASE_ENGINES,
|
|
@@ -79,8 +146,24 @@ export { DATABASE_LIFECYCLE_TAG_AGENT_NAME, PhysicalPoolIdentityError, parsePhys
|
|
|
79
146
|
export { parsePhysicalDatabaseInstanceResourceKey, type PhysicalDatabaseInstanceResourceKey } from './physicalInstanceResourceKey.js';
|
|
80
147
|
|
|
81
148
|
export type LifecycleOperation = (typeof LIFECYCLE_OPERATIONS)[number];
|
|
149
|
+
export type GenericImportLifecycleOperation = (typeof GENERIC_IMPORT_OPERATIONS)[number];
|
|
150
|
+
export type DatabaseLifecycleOperation = Exclude<LifecycleOperation, AppDatabaseImportLifecycleOperation | GenericImportLifecycleOperation>;
|
|
82
151
|
export type PoolScopedLifecycleOperation = (typeof POOL_SCOPED_LIFECYCLE_OPERATIONS)[number];
|
|
83
152
|
|
|
153
|
+
// Operations an existing agent must publish to count as covering App Database.
|
|
154
|
+
// Import operations are stored by the data-import migration, but they are not
|
|
155
|
+
// part of that default set.
|
|
156
|
+
export const DATABASE_LIFECYCLE_OPERATIONS = LIFECYCLE_OPERATIONS.filter(
|
|
157
|
+
(operation): operation is DatabaseLifecycleOperation =>
|
|
158
|
+
operation !== 'describe_schema' &&
|
|
159
|
+
!(GENERIC_IMPORT_OPERATIONS as readonly string[]).includes(operation) &&
|
|
160
|
+
!(APP_DATABASE_IMPORT_OPERATIONS as readonly string[]).includes(operation)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
export function isGenericImportLifecycleOperation(operation: string): operation is GenericImportLifecycleOperation {
|
|
164
|
+
return (GENERIC_IMPORT_OPERATIONS as readonly string[]).includes(operation);
|
|
165
|
+
}
|
|
166
|
+
|
|
84
167
|
export function isPoolScopedLifecycleOperation(operation: LifecycleOperation): operation is PoolScopedLifecycleOperation {
|
|
85
168
|
return (POOL_SCOPED_LIFECYCLE_OPERATIONS as readonly string[]).includes(operation);
|
|
86
169
|
}
|
|
@@ -99,6 +182,13 @@ export const LIFECYCLE_ERROR_CODES = [
|
|
|
99
182
|
'migration_failed',
|
|
100
183
|
'callback_failed',
|
|
101
184
|
'configuration_conflict',
|
|
185
|
+
'upload_incomplete',
|
|
186
|
+
'checksum_mismatch',
|
|
187
|
+
'size_exceeded',
|
|
188
|
+
'table_not_found',
|
|
189
|
+
'column_not_found',
|
|
190
|
+
'invalid_value',
|
|
191
|
+
'permission_denied',
|
|
102
192
|
// Server-side classification for a terminal callback that arrived for a
|
|
103
193
|
// request whose desiredSpecHash no longer matches the binding (binding has
|
|
104
194
|
// moved to a newer spec). The callback's metadata/state is discarded and
|
|
@@ -108,7 +198,54 @@ export const LIFECYCLE_ERROR_CODES = [
|
|
|
108
198
|
|
|
109
199
|
export type LifecycleErrorCode = (typeof LIFECYCLE_ERROR_CODES)[number];
|
|
110
200
|
|
|
111
|
-
export const
|
|
201
|
+
export const LIFECYCLE_IMPORT_KINDS = DATA_IMPORT_KINDS;
|
|
202
|
+
export const LIFECYCLE_IMPORT_TARGET_TYPES = DATA_IMPORT_TARGET_TYPES;
|
|
203
|
+
|
|
204
|
+
export type LifecycleImportKind = DataImportKind;
|
|
205
|
+
export type LifecycleImportTargetType = DataImportTargetType;
|
|
206
|
+
|
|
207
|
+
export type LifecycleImportTarget = DataImportTargetBase & {
|
|
208
|
+
id: string;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export type LifecycleImportPart = {
|
|
212
|
+
eTag?: string;
|
|
213
|
+
expiresAt?: string;
|
|
214
|
+
partNumber: number;
|
|
215
|
+
url?: string;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Shared fields used by prepare_import_upload and complete_import_upload.
|
|
219
|
+
// Operation-specific consumers validate which optional fields are present.
|
|
220
|
+
export type LifecycleImportPayload = {
|
|
221
|
+
expectedSizeBytes?: number;
|
|
222
|
+
filename?: string;
|
|
223
|
+
importId: string;
|
|
224
|
+
kind?: LifecycleImportKind;
|
|
225
|
+
objectKey?: string;
|
|
226
|
+
parts?: LifecycleImportPart[];
|
|
227
|
+
partSizeBytes?: number;
|
|
228
|
+
sha256?: string;
|
|
229
|
+
sizeBytes?: number;
|
|
230
|
+
target: LifecycleImportTarget;
|
|
231
|
+
uploadAlreadyComplete?: boolean;
|
|
232
|
+
uploadId?: string;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export type LifecycleTerminalCallbackPayload = {
|
|
236
|
+
bindingKey?: string;
|
|
237
|
+
connectionMetadata?: Record<string, string | number | boolean>;
|
|
238
|
+
error?: LifecycleTerminalCallbackError;
|
|
239
|
+
import?: LifecycleImportPayload;
|
|
240
|
+
lifecycleState: LifecycleTerminalState;
|
|
241
|
+
migrationState?: LifecycleMigrationState;
|
|
242
|
+
physicalConfigFingerprint?: string;
|
|
243
|
+
physicalDatabaseInstanceId?: string;
|
|
244
|
+
provisioningId?: string;
|
|
245
|
+
requestId: string;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export const LIFECYCLE_PROVIDER_DIAGNOSTIC_PHASES = ['apply', 'describe_schema', 'destroy', 'init', 'plan', 'unknown'] as const;
|
|
112
249
|
export const MAX_LIFECYCLE_PROVIDER_DIAGNOSTICS = 3;
|
|
113
250
|
export const LIFECYCLE_PROVIDER_DIAGNOSTIC_AWS_SERVICE_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
114
251
|
export const LIFECYCLE_PROVIDER_DIAGNOSTIC_AWS_OPERATION_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,63}$/;
|
|
@@ -278,6 +415,37 @@ export function sanitizeLifecycleProviderDiagnostics(value: unknown): LifecycleP
|
|
|
278
415
|
return diagnostics.length > 0 ? diagnostics : undefined;
|
|
279
416
|
}
|
|
280
417
|
|
|
418
|
+
export type LifecycleTerminalCallbackError<TErrorCode extends string = LifecycleErrorCode> = {
|
|
419
|
+
code: TErrorCode;
|
|
420
|
+
diagnostics?: LifecycleProviderDiagnostic[];
|
|
421
|
+
message: string;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
export type LifecycleTerminalCallbackEnvelope<TErrorCode extends string = LifecycleErrorCode> = {
|
|
425
|
+
bindingKey?: string;
|
|
426
|
+
connectionMetadata?: Record<string, unknown>;
|
|
427
|
+
error?: LifecycleTerminalCallbackError<TErrorCode>;
|
|
428
|
+
lifecycleState: LifecycleTerminalState;
|
|
429
|
+
migrationState?: LifecycleMigrationState;
|
|
430
|
+
requestId: string;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
type AppDatabaseImportSuccessfulCallback = Omit<
|
|
434
|
+
LifecycleTerminalCallbackEnvelope<AppDatabaseImportErrorCode>,
|
|
435
|
+
'error' | 'lifecycleState'
|
|
436
|
+
> & {
|
|
437
|
+
error?: never;
|
|
438
|
+
import: AppDatabaseImportCallbackResult;
|
|
439
|
+
lifecycleState: 'ready';
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
type AppDatabaseImportFailedCallback = Omit<LifecycleTerminalCallbackEnvelope<AppDatabaseImportErrorCode>, 'lifecycleState'> & {
|
|
443
|
+
import?: never;
|
|
444
|
+
lifecycleState: 'cancelled' | 'failed';
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export type AppDatabaseImportCallback = AppDatabaseImportFailedCallback | AppDatabaseImportSuccessfulCallback;
|
|
448
|
+
|
|
281
449
|
export type PhysicalMasterCredentialRef = {
|
|
282
450
|
resolver: 'aws_secrets_manager';
|
|
283
451
|
ref: string;
|
|
@@ -351,6 +519,24 @@ export type LifecycleRequest = {
|
|
|
351
519
|
state: LifecycleState;
|
|
352
520
|
};
|
|
353
521
|
|
|
522
|
+
export const INFRA_ABSENCE_PROOF_KINDS = ['postgres_catalog_database_absent', 'aws_rds_resource_not_found'] as const;
|
|
523
|
+
export type InfraAbsenceProofKind = (typeof INFRA_ABSENCE_PROOF_KINDS)[number];
|
|
524
|
+
|
|
525
|
+
export type LifecycleInfraAbsenceEvidence = {
|
|
526
|
+
bindingId: string;
|
|
527
|
+
checkedAt: string;
|
|
528
|
+
physicalIdentifier: string;
|
|
529
|
+
proofKind: InfraAbsenceProofKind;
|
|
530
|
+
provider: {
|
|
531
|
+
awsAccountId?: string;
|
|
532
|
+
name: 'aws_rds' | 'postgresql';
|
|
533
|
+
region?: string;
|
|
534
|
+
requestId?: string;
|
|
535
|
+
};
|
|
536
|
+
requestId: string;
|
|
537
|
+
targetDatabase: string;
|
|
538
|
+
};
|
|
539
|
+
|
|
354
540
|
// Physical database instances back the M2 shared-RDS allocation pattern: dev-DB
|
|
355
541
|
// provisioning issues `CREATE DATABASE`/`CREATE ROLE` against a pre-existing
|
|
356
542
|
// physical database instance instead of spinning up a fresh RDS per binding.
|
|
@@ -493,7 +679,7 @@ export type PhysicalPoolSummary = {
|
|
|
493
679
|
// For `ensure_physical_database_instance`, binding identity fields are empty
|
|
494
680
|
// placeholders (the OPA keys the lock on provisioningId) and `provisioningId`
|
|
495
681
|
// is required.
|
|
496
|
-
export type
|
|
682
|
+
export type DatabaseLifecycleDispatchPayload = {
|
|
497
683
|
applicationId: string;
|
|
498
684
|
bindingId: string;
|
|
499
685
|
bindingKey: string;
|
|
@@ -508,15 +694,25 @@ export type LifecycleDispatchPayload = {
|
|
|
508
694
|
// server supplies rather than anything a caller picks, and it goes away with
|
|
509
695
|
// the worker's remaining shims in ENG-5127.
|
|
510
696
|
environment?: string;
|
|
697
|
+
import?: LifecycleImportPayload;
|
|
511
698
|
metadata?: { physicalConfigFingerprint?: string };
|
|
512
699
|
migrations?: LifecycleMigration[];
|
|
513
|
-
operation:
|
|
700
|
+
operation: DatabaseLifecycleOperation;
|
|
514
701
|
profile: string;
|
|
515
702
|
provisioningId?: string;
|
|
516
703
|
requestId: string;
|
|
517
704
|
resourceKey: string;
|
|
518
705
|
};
|
|
519
706
|
|
|
707
|
+
export type PrepareImportPrefixUploadDispatchPayload = PrepareImportPrefixUploadDispatchPayloadEnvelope<DatabaseRequirement>;
|
|
708
|
+
export type InspectImportPrefixDispatchPayload = InspectImportPrefixDispatchPayloadEnvelope<DatabaseRequirement>;
|
|
709
|
+
export type AppDatabaseImportDispatchPayload = AppDatabaseImportDispatchPayloadEnvelope<DatabaseRequirement>;
|
|
710
|
+
export type GenericImportDispatchPayload = Omit<DatabaseLifecycleDispatchPayload, 'import' | 'operation'> & {
|
|
711
|
+
import: LifecycleImportPayload;
|
|
712
|
+
operation: GenericImportLifecycleOperation;
|
|
713
|
+
};
|
|
714
|
+
export type LifecycleDispatchPayload = DatabaseLifecycleDispatchPayload | GenericImportDispatchPayload | AppDatabaseImportDispatchPayload;
|
|
715
|
+
|
|
520
716
|
const PHYSICAL_POOL_PROVISIONING_RESOURCE_KEY_PREFIX = 'physical-pool-provisioning';
|
|
521
717
|
|
|
522
718
|
export function physicalPoolProvisioningResourceKey(provisioningId: string): string {
|
package/src/index.ts
CHANGED
|
@@ -28,5 +28,6 @@ export * from './jwt/index.js';
|
|
|
28
28
|
export * from './org-features/index.js';
|
|
29
29
|
export * from './database-lifecycle/index.js';
|
|
30
30
|
export * from './database-lifecycle/appDatabaseCompatibility.js';
|
|
31
|
+
export * from './database-lifecycle/appDatabaseStatus.js';
|
|
31
32
|
export * from './database-lifecycle/edit-lifecycle-capability.js';
|
|
32
33
|
export * from './environment-compatibility/appEnvironmentCompatibility.js';
|
package/src/socket/protocol.ts
CHANGED
|
@@ -36,9 +36,11 @@ import {
|
|
|
36
36
|
import {
|
|
37
37
|
AiChatMessageDto,
|
|
38
38
|
AiChatMessagePayload,
|
|
39
|
+
AiAbandonTurnEvent,
|
|
39
40
|
AiPromptCostRequest,
|
|
40
41
|
AiPromptCostResponse,
|
|
41
42
|
AiWorkflowEvent,
|
|
43
|
+
AiEditAfterClarkReport,
|
|
42
44
|
AiResolveAttachmentLeasesRequest,
|
|
43
45
|
AiResolveAttachmentLeasesResponse,
|
|
44
46
|
FactCreate,
|
|
@@ -148,9 +150,11 @@ export interface ServerMethods {
|
|
|
148
150
|
>;
|
|
149
151
|
getMessages: ServerMethodSchema<
|
|
150
152
|
{ applicationId: string; branchName?: string | null; limit?: number },
|
|
151
|
-
{ messages: AiChatMessageDto[] }
|
|
153
|
+
{ chatSessionId: string; messages: AiChatMessageDto[] }
|
|
152
154
|
>;
|
|
153
|
-
|
|
155
|
+
/** Id of the active chat session, created if the app has none yet. Reads no messages. */
|
|
156
|
+
getActiveSessionId: ServerMethodSchema<{ applicationId: string }, { chatSessionId: string }>;
|
|
157
|
+
new: ServerMethodSchema<{ applicationId: string; branchName?: string | null }, { chatSessionId: string }>;
|
|
154
158
|
truncateAfterTimestamp: ServerMethodSchema<{ applicationId: string; sinceTimestamp: number }, void>;
|
|
155
159
|
};
|
|
156
160
|
billing: {
|
|
@@ -161,6 +165,14 @@ export interface ServerMethods {
|
|
|
161
165
|
submit: ServerMethodSchema<AiWorkflowEvent, void>;
|
|
162
166
|
};
|
|
163
167
|
};
|
|
168
|
+
analytics: {
|
|
169
|
+
abandonTurn: {
|
|
170
|
+
submit: ServerMethodSchema<AiAbandonTurnEvent, void>;
|
|
171
|
+
};
|
|
172
|
+
editAfterClark: {
|
|
173
|
+
submit: ServerMethodSchema<AiEditAfterClarkReport, void>;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
164
176
|
/**
|
|
165
177
|
* Resolve attachment leases and return fresh signed URLs.
|
|
166
178
|
* Called by ai-service to refresh attachment leases before sending to LLM.
|
package/src/types/ai/index.ts
CHANGED
|
@@ -110,6 +110,45 @@ export interface AiWorkflowEvent {
|
|
|
110
110
|
deploymentName?: string;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export const AI_ABANDON_TURN_ACTIONS = ['clear_chat', 'interrupt', 'stop'] as const;
|
|
114
|
+
export type AiAbandonTurnAction = (typeof AI_ABANDON_TURN_ACTIONS)[number];
|
|
115
|
+
|
|
116
|
+
export const AI_ABANDON_TURN_MODES = ['build', 'plan'] as const;
|
|
117
|
+
export type AiAbandonTurnMode = (typeof AI_ABANDON_TURN_MODES)[number];
|
|
118
|
+
|
|
119
|
+
export const AI_ABANDON_TURN_STATES = ['AgentPlanning', 'LLMGenerating', 'PostProcessing', 'RuntimeReviewing'] as const;
|
|
120
|
+
export type AiAbandonTurnState = (typeof AI_ABANDON_TURN_STATES)[number];
|
|
121
|
+
|
|
122
|
+
export function isAiAbandonTurnState(value: unknown): value is AiAbandonTurnState {
|
|
123
|
+
return typeof value === 'string' && AI_ABANDON_TURN_STATES.some((state) => state === value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A user action that stopped or replaced an active Clark turn. */
|
|
127
|
+
export interface AiAbandonTurnEvent {
|
|
128
|
+
action: AiAbandonTurnAction;
|
|
129
|
+
actionAt: string;
|
|
130
|
+
eventId: string;
|
|
131
|
+
mode: AiAbandonTurnMode;
|
|
132
|
+
model: string;
|
|
133
|
+
provider: string;
|
|
134
|
+
/** Undefined when the client had no session established yet; unknown attribution, not an invalid event. */
|
|
135
|
+
sessionId: string | undefined;
|
|
136
|
+
state: AiAbandonTurnState;
|
|
137
|
+
turnId: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether files a completed Clark turn changed were changed again outside Clark
|
|
142
|
+
* before the next prompt. `promptId` names the Clark turn being measured, not
|
|
143
|
+
* the prompt that closed the window. Carries only counts and tree hashes, never
|
|
144
|
+
* file paths or contents. `chatSessionId` is the chat the turn belongs to, and
|
|
145
|
+
* `definitionVersion` is the version of the rule the dev server measured with.
|
|
146
|
+
*/
|
|
147
|
+
export type AiEditAfterClarkReport = { promptId: string; chatSessionId?: string; definitionVersion: number } & (
|
|
148
|
+
| { result: 'finding' | 'no_finding'; overlapCount: number; postHash: string; preHash: string }
|
|
149
|
+
| { result: 'unmeasured' }
|
|
150
|
+
);
|
|
151
|
+
|
|
113
152
|
/**
|
|
114
153
|
* Per-LLM-call token usage payload used to calculate prompt cost.
|
|
115
154
|
*/
|