@superblocksteam/shared 0.9617.8 → 0.9618.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.
Files changed (178) hide show
  1. package/dist/database-lifecycle/appDatabaseImport.d.ts +42 -7
  2. package/dist/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
  3. package/dist/database-lifecycle/appDatabaseImport.js +66 -5
  4. package/dist/database-lifecycle/appDatabaseImport.js.map +1 -1
  5. package/dist/database-lifecycle/appDatabaseImport.test.js +79 -0
  6. package/dist/database-lifecycle/appDatabaseImport.test.js.map +1 -1
  7. package/dist/database-lifecycle/appDatabaseStatus.d.ts +18 -0
  8. package/dist/database-lifecycle/appDatabaseStatus.d.ts.map +1 -1
  9. package/dist/database-lifecycle/appDatabaseStatus.js +21 -0
  10. package/dist/database-lifecycle/appDatabaseStatus.js.map +1 -1
  11. package/dist/database-lifecycle/appDatabaseStatus.test.js +11 -0
  12. package/dist/database-lifecycle/appDatabaseStatus.test.js.map +1 -1
  13. package/dist/database-lifecycle/dataImport.d.ts +45 -2
  14. package/dist/database-lifecycle/dataImport.d.ts.map +1 -1
  15. package/dist/database-lifecycle/dataImport.js +209 -3
  16. package/dist/database-lifecycle/dataImport.js.map +1 -1
  17. package/dist/database-lifecycle/dataImport.test.js +60 -0
  18. package/dist/database-lifecycle/dataImport.test.js.map +1 -1
  19. package/dist/database-lifecycle/importCompatibility.d.ts +40 -0
  20. package/dist/database-lifecycle/importCompatibility.d.ts.map +1 -0
  21. package/dist/database-lifecycle/importCompatibility.js +121 -0
  22. package/dist/database-lifecycle/importCompatibility.js.map +1 -0
  23. package/dist/database-lifecycle/importCompatibility.test.d.ts +2 -0
  24. package/dist/database-lifecycle/importCompatibility.test.d.ts.map +1 -0
  25. package/dist/database-lifecycle/importCompatibility.test.js +114 -0
  26. package/dist/database-lifecycle/importCompatibility.test.js.map +1 -0
  27. package/dist/database-lifecycle/index.d.ts +29 -5
  28. package/dist/database-lifecycle/index.d.ts.map +1 -1
  29. package/dist/database-lifecycle/index.js +18 -5
  30. package/dist/database-lifecycle/index.js.map +1 -1
  31. package/dist/database-lifecycle/index.test.js +4 -1
  32. package/dist/database-lifecycle/index.test.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/types/audit/ocsf.d.ts +2 -1
  38. package/dist/types/audit/ocsf.d.ts.map +1 -1
  39. package/dist/types/audit/ocsf.js +8 -0
  40. package/dist/types/audit/ocsf.js.map +1 -1
  41. package/dist/types/codeSecurity/index.d.ts +2 -1
  42. package/dist/types/codeSecurity/index.d.ts.map +1 -1
  43. package/dist/types/codeSecurity/index.js +43 -5
  44. package/dist/types/codeSecurity/index.js.map +1 -1
  45. package/dist/types/codeSecurity/index.test.js +37 -3
  46. package/dist/types/codeSecurity/index.test.js.map +1 -1
  47. package/dist/types/datasource/auth.d.ts +1 -0
  48. package/dist/types/datasource/auth.d.ts.map +1 -1
  49. package/dist/types/datasource/auth.js +8 -2
  50. package/dist/types/datasource/auth.js.map +1 -1
  51. package/dist/types/datasource/auth.test.js +19 -1
  52. package/dist/types/datasource/auth.test.js.map +1 -1
  53. package/dist/types/integration/index.d.ts +16 -1
  54. package/dist/types/integration/index.d.ts.map +1 -1
  55. package/dist/types/integration/index.js +8 -1
  56. package/dist/types/integration/index.js.map +1 -1
  57. package/dist/types/policyGate/index.d.ts +1 -0
  58. package/dist/types/policyGate/index.d.ts.map +1 -1
  59. package/dist/types/policyGate/index.js +3 -1
  60. package/dist/types/policyGate/index.js.map +1 -1
  61. package/dist/types/publishPipeline/index.d.ts +2 -0
  62. package/dist/types/publishPipeline/index.d.ts.map +1 -1
  63. package/dist/types/publishPipeline/index.js.map +1 -1
  64. package/dist/types/publishPipeline/plan.d.ts +1 -1
  65. package/dist/types/publishPipeline/plan.d.ts.map +1 -1
  66. package/dist/types/publishPipeline/plan.js.map +1 -1
  67. package/dist/types/rbac/index.d.ts +0 -5
  68. package/dist/types/rbac/index.d.ts.map +1 -1
  69. package/dist/types/rbac/index.js.map +1 -1
  70. package/dist/types/response/index.d.ts +0 -1
  71. package/dist/types/response/index.d.ts.map +1 -1
  72. package/dist/types/response/index.js +0 -1
  73. package/dist/types/response/index.js.map +1 -1
  74. package/dist-esm/database-lifecycle/appDatabaseImport.d.ts +42 -7
  75. package/dist-esm/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
  76. package/dist-esm/database-lifecycle/appDatabaseImport.js +65 -5
  77. package/dist-esm/database-lifecycle/appDatabaseImport.js.map +1 -1
  78. package/dist-esm/database-lifecycle/appDatabaseImport.test.js +79 -0
  79. package/dist-esm/database-lifecycle/appDatabaseImport.test.js.map +1 -1
  80. package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts +18 -0
  81. package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts.map +1 -1
  82. package/dist-esm/database-lifecycle/appDatabaseStatus.js +18 -0
  83. package/dist-esm/database-lifecycle/appDatabaseStatus.js.map +1 -1
  84. package/dist-esm/database-lifecycle/appDatabaseStatus.test.js +12 -1
  85. package/dist-esm/database-lifecycle/appDatabaseStatus.test.js.map +1 -1
  86. package/dist-esm/database-lifecycle/dataImport.d.ts +45 -2
  87. package/dist-esm/database-lifecycle/dataImport.d.ts.map +1 -1
  88. package/dist-esm/database-lifecycle/dataImport.js +205 -3
  89. package/dist-esm/database-lifecycle/dataImport.js.map +1 -1
  90. package/dist-esm/database-lifecycle/dataImport.test.js +61 -1
  91. package/dist-esm/database-lifecycle/dataImport.test.js.map +1 -1
  92. package/dist-esm/database-lifecycle/importCompatibility.d.ts +40 -0
  93. package/dist-esm/database-lifecycle/importCompatibility.d.ts.map +1 -0
  94. package/dist-esm/database-lifecycle/importCompatibility.js +117 -0
  95. package/dist-esm/database-lifecycle/importCompatibility.js.map +1 -0
  96. package/dist-esm/database-lifecycle/importCompatibility.test.d.ts +2 -0
  97. package/dist-esm/database-lifecycle/importCompatibility.test.d.ts.map +1 -0
  98. package/dist-esm/database-lifecycle/importCompatibility.test.js +112 -0
  99. package/dist-esm/database-lifecycle/importCompatibility.test.js.map +1 -0
  100. package/dist-esm/database-lifecycle/index.d.ts +29 -5
  101. package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
  102. package/dist-esm/database-lifecycle/index.js +13 -5
  103. package/dist-esm/database-lifecycle/index.js.map +1 -1
  104. package/dist-esm/database-lifecycle/index.test.js +4 -1
  105. package/dist-esm/database-lifecycle/index.test.js.map +1 -1
  106. package/dist-esm/index.d.ts +1 -0
  107. package/dist-esm/index.d.ts.map +1 -1
  108. package/dist-esm/index.js +1 -0
  109. package/dist-esm/index.js.map +1 -1
  110. package/dist-esm/types/audit/ocsf.d.ts +2 -1
  111. package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
  112. package/dist-esm/types/audit/ocsf.js +8 -0
  113. package/dist-esm/types/audit/ocsf.js.map +1 -1
  114. package/dist-esm/types/codeSecurity/index.d.ts +2 -1
  115. package/dist-esm/types/codeSecurity/index.d.ts.map +1 -1
  116. package/dist-esm/types/codeSecurity/index.js +43 -5
  117. package/dist-esm/types/codeSecurity/index.js.map +1 -1
  118. package/dist-esm/types/codeSecurity/index.test.js +37 -3
  119. package/dist-esm/types/codeSecurity/index.test.js.map +1 -1
  120. package/dist-esm/types/datasource/auth.d.ts +1 -0
  121. package/dist-esm/types/datasource/auth.d.ts.map +1 -1
  122. package/dist-esm/types/datasource/auth.js +7 -1
  123. package/dist-esm/types/datasource/auth.js.map +1 -1
  124. package/dist-esm/types/datasource/auth.test.js +20 -2
  125. package/dist-esm/types/datasource/auth.test.js.map +1 -1
  126. package/dist-esm/types/integration/index.d.ts +16 -1
  127. package/dist-esm/types/integration/index.d.ts.map +1 -1
  128. package/dist-esm/types/integration/index.js +8 -1
  129. package/dist-esm/types/integration/index.js.map +1 -1
  130. package/dist-esm/types/policyGate/index.d.ts +1 -0
  131. package/dist-esm/types/policyGate/index.d.ts.map +1 -1
  132. package/dist-esm/types/policyGate/index.js +2 -0
  133. package/dist-esm/types/policyGate/index.js.map +1 -1
  134. package/dist-esm/types/publishPipeline/index.d.ts +2 -0
  135. package/dist-esm/types/publishPipeline/index.d.ts.map +1 -1
  136. package/dist-esm/types/publishPipeline/index.js.map +1 -1
  137. package/dist-esm/types/publishPipeline/plan.d.ts +1 -1
  138. package/dist-esm/types/publishPipeline/plan.d.ts.map +1 -1
  139. package/dist-esm/types/publishPipeline/plan.js.map +1 -1
  140. package/dist-esm/types/rbac/index.d.ts +0 -5
  141. package/dist-esm/types/rbac/index.d.ts.map +1 -1
  142. package/dist-esm/types/rbac/index.js.map +1 -1
  143. package/dist-esm/types/response/index.d.ts +0 -1
  144. package/dist-esm/types/response/index.d.ts.map +1 -1
  145. package/dist-esm/types/response/index.js +0 -1
  146. package/dist-esm/types/response/index.js.map +1 -1
  147. package/package.json +2 -2
  148. package/src/database-lifecycle/appDatabaseImport.test.ts +95 -0
  149. package/src/database-lifecycle/appDatabaseImport.ts +134 -8
  150. package/src/database-lifecycle/appDatabaseStatus.test.ts +20 -1
  151. package/src/database-lifecycle/appDatabaseStatus.ts +39 -0
  152. package/src/database-lifecycle/dataImport.test.ts +75 -0
  153. package/src/database-lifecycle/dataImport.ts +267 -4
  154. package/src/database-lifecycle/importCompatibility.test.ts +146 -0
  155. package/src/database-lifecycle/importCompatibility.ts +148 -0
  156. package/src/database-lifecycle/index.test.ts +4 -1
  157. package/src/database-lifecycle/index.ts +54 -4
  158. package/src/index.ts +1 -0
  159. package/src/types/audit/ocsf.ts +10 -1
  160. package/src/types/codeSecurity/index.test.ts +40 -3
  161. package/src/types/codeSecurity/index.ts +45 -4
  162. package/src/types/datasource/auth.test.ts +25 -1
  163. package/src/types/datasource/auth.ts +9 -1
  164. package/src/types/integration/index.ts +17 -0
  165. package/src/types/policyGate/index.ts +3 -0
  166. package/src/types/publishPipeline/index.ts +2 -0
  167. package/src/types/publishPipeline/plan.ts +1 -1
  168. package/src/types/rbac/index.ts +0 -8
  169. package/src/types/response/index.ts +0 -1
  170. package/dist/types/response/GetGroupPermissionEntitiesResponseBody.d.ts +0 -3
  171. package/dist/types/response/GetGroupPermissionEntitiesResponseBody.d.ts.map +0 -1
  172. package/dist/types/response/GetGroupPermissionEntitiesResponseBody.js +0 -3
  173. package/dist/types/response/GetGroupPermissionEntitiesResponseBody.js.map +0 -1
  174. package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.d.ts +0 -3
  175. package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.d.ts.map +0 -1
  176. package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.js +0 -2
  177. package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.js.map +0 -1
  178. package/src/types/response/GetGroupPermissionEntitiesResponseBody.ts +0 -3
@@ -1,6 +1,12 @@
1
1
  import type { DataImportKind, DataImportState } from './dataImport.js';
2
2
 
3
- export const APP_DATABASE_IMPORT_OPERATIONS = ['prepare_import_prefix_upload', 'inspect_import_prefix', 'restore_import_data'] as const;
3
+ export const APP_DATABASE_IMPORT_OPERATIONS = [
4
+ 'prepare_import_prefix_upload',
5
+ 'inspect_import_prefix',
6
+ 'capture_integration_pg_dump',
7
+ 'restore_integration_pg_dump',
8
+ 'restore_import_data'
9
+ ] as const;
4
10
  // Subset of DATA_IMPORT_STATES. Prefix success is schema_validated, not ready.
5
11
  export const APP_DATABASE_IMPORT_STATES = [
6
12
  'requested',
@@ -8,7 +14,10 @@ export const APP_DATABASE_IMPORT_STATES = [
8
14
  'awaiting_prefix_upload',
9
15
  'validating_schema',
10
16
  'schema_validated',
17
+ 'capturing_source',
18
+ 'restoring',
11
19
  'uploading',
20
+ 'upload_completed',
12
21
  'importing',
13
22
  'completed',
14
23
  'failed',
@@ -26,6 +35,13 @@ export const APP_DATABASE_IMPORT_ERROR_CODES = [
26
35
  'internal',
27
36
  'invalid_input',
28
37
  'size_mismatch',
38
+ 'source_connection_failed',
39
+ 'source_dump_failed',
40
+ 'source_integration_forbidden',
41
+ 'source_integration_not_found',
42
+ 'source_integration_unsupported',
43
+ 'source_profile_unavailable',
44
+ 'source_worker_mismatch',
29
45
  'checksum_mismatch',
30
46
  'commit_uncertain',
31
47
  'stream_failed',
@@ -45,11 +61,18 @@ export type AppDatabaseImportValidationMetadata = {
45
61
  export type AppDatabaseImportDispatchDetails = {
46
62
  importId: string;
47
63
  kind: AppDatabaseImportKind;
48
- prefixSizeBytes: number;
49
64
  profileToken: string;
50
65
  resourceToken: string;
51
66
  };
52
67
 
68
+ export type AppDatabaseImportSource = {
69
+ integrationConfigurationId: string;
70
+ integrationId: string;
71
+ profile: string;
72
+ profileId: string;
73
+ type: 'integration';
74
+ };
75
+
53
76
  type AppDatabaseImportDispatchBase<TDesiredSpec> = {
54
77
  applicationId: string;
55
78
  bindingId: string;
@@ -71,17 +94,35 @@ type AppDatabaseImportDispatchBase<TDesiredSpec> = {
71
94
  export type PrepareImportPrefixUploadDispatchPayloadEnvelope<TDesiredSpec> = AppDatabaseImportDispatchBase<TDesiredSpec> & {
72
95
  import: AppDatabaseImportDispatchDetails & {
73
96
  expectedSizeBytes: number;
97
+ prefixSizeBytes: number;
74
98
  };
75
99
  operation: 'prepare_import_prefix_upload';
76
100
  };
77
101
 
78
102
  export type InspectImportPrefixDispatchPayloadEnvelope<TDesiredSpec> = AppDatabaseImportDispatchBase<TDesiredSpec> & {
79
103
  import: AppDatabaseImportDispatchDetails & {
104
+ prefixSizeBytes: number;
80
105
  prefixObjectKey: string;
81
106
  };
82
107
  operation: 'inspect_import_prefix';
83
108
  };
84
109
 
110
+ export type CaptureIntegrationPgDumpDispatchPayloadEnvelope<TDesiredSpec> = AppDatabaseImportDispatchBase<TDesiredSpec> & {
111
+ import: AppDatabaseImportDispatchDetails & {
112
+ source: AppDatabaseImportSource;
113
+ };
114
+ operation: 'capture_integration_pg_dump';
115
+ };
116
+
117
+ export type RestoreIntegrationPgDumpDispatchPayloadEnvelope<TDesiredSpec> = AppDatabaseImportDispatchBase<TDesiredSpec> & {
118
+ import: AppDatabaseImportDispatchDetails & {
119
+ objectKey: string;
120
+ sha256: string;
121
+ sizeBytes: number;
122
+ };
123
+ operation: 'restore_integration_pg_dump';
124
+ };
125
+
85
126
  export type RestoreImportDataDispatchPayloadEnvelope<TDesiredSpec> = AppDatabaseImportDispatchBase<TDesiredSpec> & {
86
127
  import: AppDatabaseImportDispatchDetails & {
87
128
  objectKey: string;
@@ -91,9 +132,11 @@ export type RestoreImportDataDispatchPayloadEnvelope<TDesiredSpec> = AppDatabase
91
132
  };
92
133
 
93
134
  export type AppDatabaseImportDispatchPayloadEnvelope<TDesiredSpec> =
135
+ | CaptureIntegrationPgDumpDispatchPayloadEnvelope<TDesiredSpec>
94
136
  | InspectImportPrefixDispatchPayloadEnvelope<TDesiredSpec>
95
137
  | PrepareImportPrefixUploadDispatchPayloadEnvelope<TDesiredSpec>
96
- | RestoreImportDataDispatchPayloadEnvelope<TDesiredSpec>;
138
+ | RestoreImportDataDispatchPayloadEnvelope<TDesiredSpec>
139
+ | RestoreIntegrationPgDumpDispatchPayloadEnvelope<TDesiredSpec>;
97
140
 
98
141
  export type PrepareImportPrefixUploadCallbackResult = {
99
142
  prefixObjectKey: string;
@@ -114,7 +157,25 @@ export type InspectImportPrefixCallbackResult =
114
157
  validationMetadata: AppDatabaseImportValidationMetadata;
115
158
  };
116
159
 
117
- export type AppDatabaseImportCallbackResult = InspectImportPrefixCallbackResult | PrepareImportPrefixUploadCallbackResult;
160
+ export type CaptureIntegrationPgDumpCallbackResult = {
161
+ artifact: {
162
+ objectKey: string;
163
+ sha256: string;
164
+ sizeBytes: number;
165
+ };
166
+ result: 'integration_pg_dump_captured';
167
+ };
168
+
169
+ export type RestoreIntegrationPgDumpCallbackResult = {
170
+ result: 'integration_pg_dump_restored';
171
+ validationMetadata: AppDatabaseImportValidationMetadata;
172
+ };
173
+
174
+ export type AppDatabaseImportCallbackResult =
175
+ | CaptureIntegrationPgDumpCallbackResult
176
+ | InspectImportPrefixCallbackResult
177
+ | PrepareImportPrefixUploadCallbackResult
178
+ | RestoreIntegrationPgDumpCallbackResult;
118
179
 
119
180
  const importOperations = new Set<string>(APP_DATABASE_IMPORT_OPERATIONS);
120
181
 
@@ -131,8 +192,26 @@ export function isAppDatabaseImportDispatchPayload(
131
192
  if (!isNonEmptyString(value.import.profileToken) || !isNonEmptyString(value.import.resourceToken)) {
132
193
  return false;
133
194
  }
195
+ if (value.operation === 'capture_integration_pg_dump') {
196
+ return (
197
+ hasOnlyKeys(value.import, ['importId', 'kind', 'profileToken', 'resourceToken', 'source']) &&
198
+ isAppDatabaseImportSource(value.import.source)
199
+ );
200
+ }
201
+ if (value.operation === 'restore_integration_pg_dump') {
202
+ return (
203
+ hasOnlyKeys(value.import, ['importId', 'kind', 'objectKey', 'profileToken', 'resourceToken', 'sha256', 'sizeBytes']) &&
204
+ isSafeObjectKey(value.import.objectKey) &&
205
+ isPositiveInteger(value.import.sizeBytes) &&
206
+ isSha256(value.import.sha256)
207
+ );
208
+ }
134
209
  if (value.operation === 'prepare_import_prefix_upload') {
135
- return value.import.prefixObjectKey === undefined && isPositiveInteger(value.import.expectedSizeBytes);
210
+ return (
211
+ hasOnlyKeys(value.import, ['expectedSizeBytes', 'importId', 'kind', 'prefixSizeBytes', 'profileToken', 'resourceToken']) &&
212
+ isPositiveInteger(value.import.expectedSizeBytes) &&
213
+ isPositiveInteger(value.import.prefixSizeBytes)
214
+ );
136
215
  }
137
216
  if (value.operation === 'restore_import_data') {
138
217
  return (
@@ -144,7 +223,8 @@ export function isAppDatabaseImportDispatchPayload(
144
223
  }
145
224
  return (
146
225
  value.operation === 'inspect_import_prefix' &&
147
- value.import.expectedSizeBytes === undefined &&
226
+ hasOnlyKeys(value.import, ['importId', 'kind', 'prefixObjectKey', 'prefixSizeBytes', 'profileToken', 'resourceToken']) &&
227
+ isPositiveInteger(value.import.prefixSizeBytes) &&
148
228
  isNonEmptyString(value.import.prefixObjectKey)
149
229
  );
150
230
  }
@@ -165,6 +245,22 @@ export function isAppDatabaseImportCallbackResult(value: unknown): value is AppD
165
245
  value.nextPrefixSizeBytes > value.inspectedPrefixSizeBytes
166
246
  );
167
247
  }
248
+ if (value.result === 'integration_pg_dump_captured') {
249
+ return (
250
+ hasOnlyKeys(value, ['artifact', 'result']) &&
251
+ isUnknownRecord(value.artifact) &&
252
+ hasOnlyKeys(value.artifact, ['objectKey', 'sha256', 'sizeBytes']) &&
253
+ isSafeObjectKey(value.artifact.objectKey) &&
254
+ isPositiveInteger(value.artifact.sizeBytes) &&
255
+ isSha256(value.artifact.sha256)
256
+ );
257
+ }
258
+ if (value.result === 'integration_pg_dump_restored') {
259
+ return (
260
+ hasOnlyKeys(value, ['result', 'validationMetadata']) &&
261
+ parseAppDatabaseImportValidationMetadata(value.validationMetadata, true) !== undefined
262
+ );
263
+ }
168
264
  return (
169
265
  value.result === 'schema_validated' &&
170
266
  parseAppDatabaseImportValidationMetadata(value.validationMetadata) !== undefined &&
@@ -172,11 +268,27 @@ export function isAppDatabaseImportCallbackResult(value: unknown): value is AppD
172
268
  );
173
269
  }
174
270
 
271
+ export function isAppDatabaseImportSource(value: unknown): value is AppDatabaseImportSource {
272
+ return (
273
+ isUnknownRecord(value) &&
274
+ hasOnlyKeys(value, ['integrationConfigurationId', 'integrationId', 'profile', 'profileId', 'type']) &&
275
+ value.type === 'integration' &&
276
+ isNonEmptyString(value.integrationId) &&
277
+ isNonEmptyString(value.integrationConfigurationId) &&
278
+ isNonEmptyString(value.profileId) &&
279
+ isNonEmptyString(value.profile)
280
+ );
281
+ }
282
+
175
283
  // Rebuild metadata from its allowlisted fields so archive contents, SQL, rows,
176
284
  // and other worker-provided details cannot cross this contract boundary.
177
- export function parseAppDatabaseImportValidationMetadata(value: unknown): AppDatabaseImportValidationMetadata | undefined {
285
+ export function parseAppDatabaseImportValidationMetadata(
286
+ value: unknown,
287
+ requireExact = false
288
+ ): AppDatabaseImportValidationMetadata | undefined {
178
289
  if (
179
290
  !isUnknownRecord(value) ||
291
+ (requireExact && !hasOnlyKeys(value, ['postgresMajorVersion', 'tableNames'])) ||
180
292
  !isPositiveInteger(value.postgresMajorVersion) ||
181
293
  !Array.isArray(value.tableNames) ||
182
294
  !value.tableNames.every(isNonEmptyString)
@@ -203,7 +315,6 @@ function hasImportDispatchBase(value: Record<string, unknown>): value is ImportD
203
315
  isUnknownRecord(value.import) &&
204
316
  isNonEmptyString(value.import.importId) &&
205
317
  value.import.kind === 'pg_dump_custom' &&
206
- isPositiveInteger(value.import.prefixSizeBytes) &&
207
318
  isNonEmptyString(value.organizationId) &&
208
319
  isNonEmptyString(value.profile) &&
209
320
  isNonEmptyString(value.requestId) &&
@@ -233,6 +344,21 @@ function isPositiveInteger(value: unknown): value is number {
233
344
  return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
234
345
  }
235
346
 
347
+ function isSha256(value: unknown): value is string {
348
+ return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
349
+ }
350
+
351
+ function isSafeObjectKey(value: unknown): value is string {
352
+ return (
353
+ typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._/-]{0,1023}$/.test(value) && !value.split('/').some((segment) => segment === '..')
354
+ );
355
+ }
356
+
236
357
  function isUnknownRecord(value: unknown): value is Record<string, unknown> {
237
358
  return value !== null && typeof value === 'object' && !Array.isArray(value);
238
359
  }
360
+
361
+ function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
362
+ const allowedKeys = new Set(allowed);
363
+ return Object.keys(value).every((key) => allowedKeys.has(key));
364
+ }
@@ -1,9 +1,17 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import { ViewMode } from '../types/event/index.js';
4
- import { isProductionDatabaseRetirementEligible, type AppDatabaseStatus, type AppDatabaseStatusBinding } from './appDatabaseStatus.js';
4
+ import {
5
+ databaseWideOperationBlockReason,
6
+ isProductionDatabaseRetirementEligible,
7
+ preferredBlockingDatabaseOperation,
8
+ TABLE_IMPORT_IN_PROGRESS_REASON,
9
+ type AppDatabaseStatus,
10
+ type AppDatabaseStatusBinding
11
+ } from './appDatabaseStatus.js';
5
12
 
6
13
  const binding: AppDatabaseStatusBinding = {
14
+ agentName: 'customer-opa',
7
15
  bindingId: 'binding-1',
8
16
  created: '2026-09-14T00:00:00.000Z',
9
17
  instance: null,
@@ -103,3 +111,14 @@ describe('production database retirement eligibility', () => {
103
111
  ).toBe(false);
104
112
  });
105
113
  });
114
+
115
+ describe('database-wide operation block', () => {
116
+ it('prefers retirement over a migration and names the operation the import has to wait for', () => {
117
+ expect(preferredBlockingDatabaseOperation(['restore_import_data', 'migrate_schema', 'retire_database'])).toBe('retire_database');
118
+ expect(preferredBlockingDatabaseOperation(['import_table_data'])).toBeUndefined();
119
+ expect(databaseWideOperationBlockReason('migrate_schema')).toBe('A schema migration is running on this database.');
120
+ expect(databaseWideOperationBlockReason('restore_import_data')).toBe('A database restore is running on this data tag.');
121
+ expect(databaseWideOperationBlockReason('retire_database')).toBe('This database is being removed.');
122
+ expect(TABLE_IMPORT_IN_PROGRESS_REASON).toBe('A table import is running on this database.');
123
+ });
124
+ });
@@ -1,5 +1,6 @@
1
1
  import { ViewMode } from '../types/event/index.js';
2
2
  import type { AppDatabaseTagRef } from './appDatabaseCompatibility.js';
3
+ import type { AppDatabaseImportTagSupport } from './importCompatibility.js';
3
4
  import type { DatabaseEngine, LifecycleMigrationState, LifecycleState } from './index.js';
4
5
 
5
6
  /**
@@ -20,13 +21,46 @@ export type AppDatabaseStatusIntegration = {
20
21
  pluginId: string;
21
22
  };
22
23
 
24
+ // These rewrite or remove the whole database. A CSV import into any table on
25
+ // that binding has to wait until the lifecycle request leaves these states.
26
+ export const DATABASE_WIDE_LIFECYCLE_OPERATIONS = ['migrate_schema', 'restore_import_data', 'retire_database'] as const;
27
+ export type DatabaseWideLifecycleOperation = (typeof DATABASE_WIDE_LIFECYCLE_OPERATIONS)[number];
28
+
29
+ export const TABLE_IMPORT_IN_PROGRESS_REASON = 'A table import is running on this database.';
30
+
31
+ export function databaseWideOperationBlockReason(operation: DatabaseWideLifecycleOperation): string {
32
+ switch (operation) {
33
+ case 'migrate_schema':
34
+ return 'A schema migration is running on this database.';
35
+ case 'restore_import_data':
36
+ return 'A database restore is running on this data tag.';
37
+ case 'retire_database':
38
+ return 'This database is being removed.';
39
+ }
40
+ }
41
+
42
+ const BLOCKING_OPERATION_PRIORITY = ['retire_database', 'migrate_schema', 'restore_import_data'] as const;
43
+
44
+ export function preferredBlockingDatabaseOperation(operations: readonly string[]): DatabaseWideLifecycleOperation | undefined {
45
+ return BLOCKING_OPERATION_PRIORITY.find((operation) => operations.includes(operation));
46
+ }
47
+
23
48
  export type AppDatabaseStatusBinding = {
49
+ /**
50
+ * Owning OPA deployment name (`databaseLifecycle:agentName`). Null on
51
+ * bindings created before ownership tracking. Schema reads route to this
52
+ * agent; without it the browser cannot authorize a describe call.
53
+ */
54
+ agentName: string | null;
24
55
  bindingId: string;
25
56
  created: string;
26
57
  instance: AppDatabaseStatusInstance | null;
27
58
  integration: AppDatabaseStatusIntegration | null;
28
59
  lastActivity: string;
29
60
  lifecycleState: LifecycleState;
61
+ // Set while migrate_schema, restore_import_data, or retire_database is still
62
+ // pending or running for this binding.
63
+ inProgressOperation?: DatabaseWideLifecycleOperation;
30
64
  logicalName: string;
31
65
  migrationDirectory: string | null;
32
66
  migrationState: LifecycleMigrationState;
@@ -35,6 +69,11 @@ export type AppDatabaseStatusBinding = {
35
69
  export type AppDatabaseStatusTag = AppDatabaseTagRef & {
36
70
  /** Null when this data tag has no live (non-cancelled) database. */
37
71
  binding: AppDatabaseStatusBinding | null;
72
+ /**
73
+ * Whether this tag's registered worker can run a CSV import. Computed by
74
+ * the server from connected-agent registrations, not from a browser probe.
75
+ */
76
+ csvImport?: AppDatabaseImportTagSupport;
38
77
  /**
39
78
  * True when this data tag had a database that was retired. The live
40
79
  * binding is omitted so the tag can be provisioned again, but Status
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
 
3
3
  import {
4
+ csvImportPlanFromWizard,
4
5
  mintDataImportConcurrencyKey,
5
6
  parseDataImportMetadata,
6
7
  parseDataImportTarget,
@@ -62,6 +63,12 @@ describe('data import contracts', () => {
62
63
  }
63
64
  expect(mintDataImportConcurrencyKey(spaced, target)).toBe(`csv:${target.bindingId}:public.orders`);
64
65
  expect(mintDataImportConcurrencyKey(plain, target)).toBe(mintDataImportConcurrencyKey(spaced, target));
66
+ expect(mintDataImportConcurrencyKey({ kind: 'csv', mode: 'append', table: 'public.users' }, target)).not.toBe(
67
+ mintDataImportConcurrencyKey(plain, target)
68
+ );
69
+ expect(mintDataImportConcurrencyKey(plain, { ...target, bindingId: 'binding-production' })).not.toBe(
70
+ mintDataImportConcurrencyKey(plain, target)
71
+ );
65
72
  expect(parseDataImportMetadata('csv', { kind: 'csv', mode: 'append', table: ' ' })).toBeUndefined();
66
73
  expect(parseDataImportMetadata('csv', { kind: 'csv', mode: 'append', table: 'public. order items' })).toBeUndefined();
67
74
  for (const table of ['PUBLIC.ORDERS', 'public."orders"', '"public"."orders"', 'orders']) {
@@ -80,6 +87,74 @@ describe('data import contracts', () => {
80
87
  expect(parseDataImportMetadata('csv', { kind: 'csv', mode: 'append', table: `public.${sixtyThree}y` })).toBeUndefined();
81
88
  });
82
89
 
90
+ test('keeps a csv upload without a table plan and stores a complete plan', () => {
91
+ expect(parseDataImportMetadata('csv', { kind: 'csv', mode: 'append', table: 'public.orders' })).toEqual({
92
+ kind: 'csv',
93
+ mode: 'append',
94
+ table: 'public.orders'
95
+ });
96
+ const plan = {
97
+ columnMapping: [{ source: 'id', target: 'id' }],
98
+ dateStyle: 'ISO, YMD',
99
+ delimiter: ',' as const,
100
+ emptyAsNull: true,
101
+ encoding: 'UTF8' as const,
102
+ hasHeader: true,
103
+ timeZone: 'UTC',
104
+ trimWhitespace: true
105
+ };
106
+ expect(parseDataImportMetadata('csv', { kind: 'csv', mode: 'replace', table: 'public.orders', ...plan })).toEqual({
107
+ kind: 'csv',
108
+ mode: 'replace',
109
+ table: 'public.orders',
110
+ ...plan
111
+ });
112
+ expect(
113
+ parseDataImportMetadata('csv', { kind: 'csv', mode: 'append', table: 'public.superblocks_schema_migrations', ...plan })
114
+ ).toBeUndefined();
115
+ expect(parseDataImportMetadata('csv', { kind: 'csv', mode: 'append', table: 'public.superblocks_schema_migrations' })).toBeUndefined();
116
+ expect(parseDataImportMetadata('csv', { kind: 'csv', mode: 'append', table: 'pg_catalog.pg_class', ...plan })).toBeUndefined();
117
+ });
118
+
119
+ test('maps wizard date formats and encodings onto the worker plan', () => {
120
+ const mapped = csvImportPlanFromWizard({
121
+ columnMappings: { id: 'id', skipped: '' },
122
+ dateFormat: 'YYYY-MM-DD',
123
+ encoding: 'utf-8',
124
+ trimWhitespace: true
125
+ });
126
+ expect(mapped).toEqual({
127
+ plan: {
128
+ columnMapping: [{ source: 'id', target: 'id' }],
129
+ dateStyle: 'ISO, YMD',
130
+ delimiter: ',',
131
+ emptyAsNull: true,
132
+ encoding: 'UTF8',
133
+ hasHeader: true,
134
+ timeZone: 'UTC',
135
+ trimWhitespace: true
136
+ },
137
+ status: 'ok'
138
+ });
139
+ expect(
140
+ csvImportPlanFromWizard({
141
+ columnMappings: { id: 'id' },
142
+ dateFormat: 'MM/DD/YYYY',
143
+ encoding: 'windows-1252',
144
+ trimWhitespace: false
145
+ })
146
+ ).toMatchObject({ plan: { dateStyle: 'ISO, MDY', encoding: 'WINDOWS1252', trimWhitespace: false }, status: 'ok' });
147
+ expect(
148
+ csvImportPlanFromWizard({ columnMappings: { id: 'id' }, dateFormat: 'YYYY-MM-DD', encoding: 'utf-16', trimWhitespace: true })
149
+ ).toEqual({ message: 'Encoding must be UTF-8 or Windows-1252.', status: 'invalid' });
150
+ expect(
151
+ csvImportPlanFromWizard({ columnMappings: { id: '' }, dateFormat: 'not-a-date', encoding: 'utf-8', trimWhitespace: true })
152
+ ).toEqual({
153
+ message: 'Date format must be YYYY-MM-DD, MM/DD/YYYY, DD/MM/YYYY, or DD.MM.YYYY.',
154
+ status: 'invalid'
155
+ });
156
+ });
157
+
83
158
  test('rejects zero prefix sizes and a zero postgres major', () => {
84
159
  const base = { kind: 'pg_dump_custom' as const };
85
160
  expect(parseDataImportMetadata('pg_dump_custom', { ...base, currentPrefixSizeBytes: 0 })).toBeUndefined();