@friggframework/core 2.0.0-next.102 → 2.0.0-next.104
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/README.md +40 -0
- package/application/commands/usage-commands.js +56 -0
- package/application/index.js +10 -9
- package/core/create-handler.js +112 -10
- package/database/use-cases/resolve-migration-via-worker-use-case.js +49 -0
- package/database/utils/prisma-runner.js +16 -2
- package/generated/prisma-mongodb/edge.js +16 -4
- package/generated/prisma-mongodb/index-browser.js +13 -1
- package/generated/prisma-mongodb/index.d.ts +1503 -105
- package/generated/prisma-mongodb/index.js +16 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +23 -0
- package/generated/prisma-mongodb/wasm.js +16 -4
- package/generated/prisma-postgresql/edge.js +16 -4
- package/generated/prisma-postgresql/index-browser.js +13 -1
- package/generated/prisma-postgresql/index.d.ts +1540 -91
- package/generated/prisma-postgresql/index.js +16 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +22 -0
- package/generated/prisma-postgresql/wasm.js +16 -4
- package/handlers/app-definition-loader.js +26 -3
- package/handlers/integration-event-dispatcher.js +29 -15
- package/handlers/routers/db-migration.js +36 -18
- package/handlers/routers/integration-webhook-routers.js +20 -7
- package/handlers/workers/db-migration.js +75 -0
- package/index.js +16 -9
- package/integrations/integration-base.js +64 -7
- package/modules/requester/requester.js +106 -5
- package/package.json +12 -5
- package/prisma-mongodb/schema.prisma +23 -0
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
- package/prisma-postgresql/schema.prisma +22 -0
- package/reporting/README.md +8 -1
- package/reporting/reporting-router.js +8 -1
- package/reporting/use-cases/list-integrations-report.js +53 -6
- package/telemetry/README.md +331 -0
- package/telemetry/bind-telemetry-context.js +73 -0
- package/telemetry/canonical-counters.js +52 -0
- package/telemetry/exporters.js +85 -0
- package/telemetry/index.js +26 -0
- package/telemetry/instrument-handler.js +87 -0
- package/telemetry/no-op-telemetry.js +67 -0
- package/telemetry/north-star.js +103 -0
- package/telemetry/otel-telemetry.js +213 -0
- package/telemetry/plugin-subscribers.js +77 -0
- package/telemetry/telemetry-config.js +120 -0
- package/telemetry/telemetry-context.js +40 -0
- package/telemetry/telemetry-event-bus.js +58 -0
- package/telemetry/telemetry-runtime.js +147 -0
- package/telemetry/telemetry-service.js +51 -0
- package/telemetry/usage-rollup-subscriber.js +116 -0
- package/usage/README.md +54 -0
- package/usage/index.js +17 -0
- package/usage/repositories/usage-repository-documentdb.js +194 -0
- package/usage/repositories/usage-repository-factory.js +25 -0
- package/usage/repositories/usage-repository-interface.js +37 -0
- package/usage/repositories/usage-repository-prisma.js +146 -0
- package/usage/tracked-metrics.js +38 -0
- package/usage/usage-windows.js +24 -0
package/README.md
CHANGED
|
@@ -200,6 +200,46 @@ const secureData = cryptor.encrypt(JSON.stringify({
|
|
|
200
200
|
}));
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
### 4b. Telemetry & Usage Tracking (`/telemetry`, `/usage`)
|
|
204
|
+
|
|
205
|
+
Vendor-neutral OpenTelemetry observability plus durable, per-integration usage
|
|
206
|
+
counters (ADR-011). No-op by default (zero cold-start cost); framework seams are
|
|
207
|
+
auto-instrumented so integrations get handler/API-module/webhook metrics for free.
|
|
208
|
+
|
|
209
|
+
**Usage:**
|
|
210
|
+
```javascript
|
|
211
|
+
// App definition — turn on export + declare a North Star:
|
|
212
|
+
const Definition = {
|
|
213
|
+
name: 'my-app',
|
|
214
|
+
telemetry: {
|
|
215
|
+
exporter: { type: 'otlp', endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT },
|
|
216
|
+
northStar: { default: { name: 'records.synced' } },
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Integration code — custom metrics/spans (this.telemetry is auto-tagged):
|
|
221
|
+
await this.telemetry.span('delta_sync', async () => {
|
|
222
|
+
this.telemetry.count('records.synced', batch.length, { entity: 'contact' });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Declare which usage counters an integration reports (opts into reporting):
|
|
226
|
+
class HubSpotIntegration extends IntegrationBase {
|
|
227
|
+
static Definition = {
|
|
228
|
+
name: 'hubspot',
|
|
229
|
+
usage: { canonical: ['records.synced', 'api.requests'] },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Read the durable usage store (never an APM):
|
|
234
|
+
const frigg = createFriggCommands({ integrationClass: HubSpotIntegration });
|
|
235
|
+
await frigg.usage.getTotalsByDimension({ metric: 'records.synced', groupBy: 'integrationType' });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**See:** [`telemetry/README.md`](telemetry/README.md) for the full guide
|
|
239
|
+
(exporters, custom metrics, the Usage-Counter contract, North Star, the plugin
|
|
240
|
+
tap, cardinality rules, and caveats) and [`usage/README.md`](usage/README.md) for
|
|
241
|
+
the store internals.
|
|
242
|
+
|
|
203
243
|
### 5. Error Handling (`/errors`)
|
|
204
244
|
|
|
205
245
|
Standardized error types with proper HTTP status codes.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// `frigg.usage.*` — the read/write surface over the durable usage store.
|
|
2
|
+
const {
|
|
3
|
+
createUsageRepository,
|
|
4
|
+
} = require('../../usage/repositories/usage-repository-factory');
|
|
5
|
+
const { computeUsageWindows } = require('../../usage/usage-windows');
|
|
6
|
+
const { resolveNorthStarEntry } = require('../../telemetry/north-star');
|
|
7
|
+
|
|
8
|
+
function createUsageCommands({ usageRepository } = {}) {
|
|
9
|
+
const repository = usageRepository || createUsageRepository();
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
// Increments both the day and hour window rows for `at`.
|
|
13
|
+
async recordUsageCounter({
|
|
14
|
+
integrationId,
|
|
15
|
+
integrationType,
|
|
16
|
+
metric,
|
|
17
|
+
value = 1,
|
|
18
|
+
at = new Date(),
|
|
19
|
+
}) {
|
|
20
|
+
const windows = computeUsageWindows(at);
|
|
21
|
+
for (const window of windows) {
|
|
22
|
+
await repository.increment({
|
|
23
|
+
integrationId,
|
|
24
|
+
integrationType,
|
|
25
|
+
metric,
|
|
26
|
+
window,
|
|
27
|
+
value,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async getTotalsByDimension(args) {
|
|
33
|
+
return repository.getTotalsByDimension(args);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async getTimeSeries(args) {
|
|
37
|
+
return repository.getTimeSeries(args);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Caller supplies the North Star config (Definition.telemetry.northStar);
|
|
41
|
+
// resolves the counter for the type (byType > default), null if none.
|
|
42
|
+
async getNorthStarTotals({ northStar, integrationType, since, groupBy = 'integrationType', bucket } = {}) {
|
|
43
|
+
const entry = resolveNorthStarEntry(northStar, integrationType);
|
|
44
|
+
if (!entry) return null;
|
|
45
|
+
const totals = await repository.getTotalsByDimension({
|
|
46
|
+
metric: entry.name,
|
|
47
|
+
groupBy,
|
|
48
|
+
since,
|
|
49
|
+
bucket,
|
|
50
|
+
});
|
|
51
|
+
return { metric: entry.name, totals };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { createUsageCommands };
|
package/application/index.js
CHANGED
|
@@ -4,15 +4,10 @@ const {
|
|
|
4
4
|
} = require('./commands/integration-commands');
|
|
5
5
|
const { createUserCommands } = require('./commands/user-commands');
|
|
6
6
|
const { createEntityCommands } = require('./commands/entity-commands');
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
} = require('./commands/
|
|
10
|
-
const {
|
|
11
|
-
createProcessCommands,
|
|
12
|
-
} = require('./commands/process-commands');
|
|
13
|
-
const {
|
|
14
|
-
createSchedulerCommands,
|
|
15
|
-
} = require('./commands/scheduler-commands');
|
|
7
|
+
const { createCredentialCommands } = require('./commands/credential-commands');
|
|
8
|
+
const { createProcessCommands } = require('./commands/process-commands');
|
|
9
|
+
const { createSchedulerCommands } = require('./commands/scheduler-commands');
|
|
10
|
+
const { createUsageCommands } = require('./commands/usage-commands');
|
|
16
11
|
|
|
17
12
|
/**
|
|
18
13
|
* Create a unified command factory with all CRUD operations
|
|
@@ -56,6 +51,11 @@ function createFriggCommands({ integrationClass }) {
|
|
|
56
51
|
|
|
57
52
|
// Process commands
|
|
58
53
|
...processCommands,
|
|
54
|
+
|
|
55
|
+
// Usage read/write — nested to match `frigg.usage.*`. The North Star
|
|
56
|
+
// read takes its config as a call argument, so nothing telemetry-specific
|
|
57
|
+
// is threaded through this general factory.
|
|
58
|
+
usage: createUsageCommands(),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -70,6 +70,7 @@ module.exports = {
|
|
|
70
70
|
createCredentialCommands,
|
|
71
71
|
createProcessCommands,
|
|
72
72
|
createSchedulerCommands,
|
|
73
|
+
createUsageCommands,
|
|
73
74
|
|
|
74
75
|
// Legacy standalone function
|
|
75
76
|
findIntegrationContextByExternalEntityId,
|
package/core/create-handler.js
CHANGED
|
@@ -5,6 +5,91 @@
|
|
|
5
5
|
const { initDebugLog, flushDebugLog } = require('../logs');
|
|
6
6
|
const { secretsToEnv } = require('./secrets-to-env');
|
|
7
7
|
const { parametersToEnv } = require('./parameters-to-env');
|
|
8
|
+
const {
|
|
9
|
+
getTelemetry,
|
|
10
|
+
getUsageRollupSubscriber,
|
|
11
|
+
getPluginTelemetrySubscribers,
|
|
12
|
+
} = require('../telemetry/telemetry-runtime');
|
|
13
|
+
|
|
14
|
+
// Bounds the tail latency telemetry adds to every warm invocation. Kept low so
|
|
15
|
+
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
16
|
+
// most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
|
|
17
|
+
const DEFAULT_FLUSH_TIMEOUT_MS =
|
|
18
|
+
Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fold the invocation's buffered usage counters into the durable store, then
|
|
22
|
+
* clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
|
|
23
|
+
* DISCARD rather than flush — the prior delivery already counted, and the usage
|
|
24
|
+
* accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
|
|
25
|
+
*/
|
|
26
|
+
async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
|
|
27
|
+
if (!subscriber) return;
|
|
28
|
+
try {
|
|
29
|
+
// Persisting usage requires a DB connection. DB-free handlers (e.g. the
|
|
30
|
+
// webhook-receipt route) never called connectPrisma, so drop the buffer
|
|
31
|
+
// instead of issuing a connectionless Prisma write.
|
|
32
|
+
if (!shouldUseDatabase) {
|
|
33
|
+
subscriber.discard();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Discard only when EVERY record in the batch is a redelivery. The buffer
|
|
37
|
+
// is invocation-scoped (not per-message), so discarding on *any*
|
|
38
|
+
// redelivery would drop the fresh records' counts too (silent
|
|
39
|
+
// under-count). For a mixed batch we flush: preserving fresh counts and
|
|
40
|
+
// at worst re-counting the one redelivered record is strictly better than
|
|
41
|
+
// losing fresh data for an approximate store. (Integration queue workers
|
|
42
|
+
// are batchSize:1 today, so a batch is all-or-nothing; this keeps it
|
|
43
|
+
// correct if batchSize is ever raised.)
|
|
44
|
+
const records = Array.isArray(eventSummary?.records)
|
|
45
|
+
? eventSummary.records
|
|
46
|
+
: [];
|
|
47
|
+
const allRedelivered =
|
|
48
|
+
records.length > 0 &&
|
|
49
|
+
records.every((r) => Number(r.receiveCount) > 1);
|
|
50
|
+
if (allRedelivered) {
|
|
51
|
+
subscriber.discard();
|
|
52
|
+
} else {
|
|
53
|
+
await subscriber.flush();
|
|
54
|
+
}
|
|
55
|
+
} catch (_) {
|
|
56
|
+
// Usage rollup must never break the handler.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Flush telemetry before the Lambda container freezes. Because
|
|
62
|
+
* `callbackWaitsForEmptyEventLoop=false` (below) stops the event loop the moment
|
|
63
|
+
* the handler returns, OTel's timer-driven batch processors would never fire —
|
|
64
|
+
* so spans/metrics must be flushed synchronously here. Bounded by a timeout so a
|
|
65
|
+
* stalled exporter can never block the response, and fully guarded so a flush
|
|
66
|
+
* failure never breaks the handler.
|
|
67
|
+
*/
|
|
68
|
+
async function flushTelemetry(telemetry, timeoutMs) {
|
|
69
|
+
try {
|
|
70
|
+
if (
|
|
71
|
+
!telemetry ||
|
|
72
|
+
typeof telemetry.isEnabled !== 'function' ||
|
|
73
|
+
!telemetry.isEnabled()
|
|
74
|
+
) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let timer;
|
|
78
|
+
const deadline = new Promise((resolve) => {
|
|
79
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
await Promise.race([
|
|
83
|
+
Promise.resolve(telemetry.forceFlush()),
|
|
84
|
+
deadline,
|
|
85
|
+
]);
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
} catch (_) {
|
|
90
|
+
// Telemetry flush must never break the handler.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
8
93
|
|
|
9
94
|
// Best-effort extraction of correlation identifiers from a Lambda event.
|
|
10
95
|
// For SQS: pulls messageIds + parsed event/processId/integrationId from each
|
|
@@ -37,8 +122,7 @@ const summarizeLambdaEvent = (event) => {
|
|
|
37
122
|
if (event.httpMethod || event.requestContext?.http) {
|
|
38
123
|
return {
|
|
39
124
|
source: 'http',
|
|
40
|
-
method:
|
|
41
|
-
event.httpMethod || event.requestContext?.http?.method,
|
|
125
|
+
method: event.httpMethod || event.requestContext?.http?.method,
|
|
42
126
|
path: event.path || event.rawPath,
|
|
43
127
|
};
|
|
44
128
|
}
|
|
@@ -51,6 +135,9 @@ const createHandler = (optionByName = {}) => {
|
|
|
51
135
|
isUserFacingResponse = true,
|
|
52
136
|
method,
|
|
53
137
|
shouldUseDatabase = true,
|
|
138
|
+
telemetry,
|
|
139
|
+
flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
|
|
140
|
+
usageRollup,
|
|
54
141
|
} = optionByName;
|
|
55
142
|
|
|
56
143
|
if (!method) {
|
|
@@ -59,16 +146,23 @@ const createHandler = (optionByName = {}) => {
|
|
|
59
146
|
|
|
60
147
|
return async (event, context) => {
|
|
61
148
|
const eventSummary = summarizeLambdaEvent(event);
|
|
149
|
+
const activeTelemetry = telemetry || getTelemetry();
|
|
150
|
+
const activeUsageRollup =
|
|
151
|
+
usageRollup !== undefined
|
|
152
|
+
? usageRollup
|
|
153
|
+
: getUsageRollupSubscriber();
|
|
154
|
+
|
|
155
|
+
// Wire adopter-declared telemetry subscribers once per cold start.
|
|
156
|
+
// Memoized in the singleton, so this is a cheap
|
|
157
|
+
// no-op after the first invocation.
|
|
158
|
+
getPluginTelemetrySubscribers();
|
|
62
159
|
|
|
63
160
|
try {
|
|
64
|
-
console.info(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
...eventSummary,
|
|
70
|
-
}
|
|
71
|
-
);
|
|
161
|
+
console.info(`[createHandler] ${eventName}: handler entry`, {
|
|
162
|
+
eventName,
|
|
163
|
+
awsRequestId: context?.awsRequestId,
|
|
164
|
+
...eventSummary,
|
|
165
|
+
});
|
|
72
166
|
|
|
73
167
|
initDebugLog(eventName, event);
|
|
74
168
|
|
|
@@ -143,6 +237,14 @@ const createHandler = (optionByName = {}) => {
|
|
|
143
237
|
|
|
144
238
|
// Here we can just rethrow and let AWS build the response.
|
|
145
239
|
throw error;
|
|
240
|
+
} finally {
|
|
241
|
+
// Flush telemetry + usage before the container freezes.
|
|
242
|
+
await flushTelemetry(activeTelemetry, flushTimeoutMs);
|
|
243
|
+
await flushUsageRollup(
|
|
244
|
+
activeUsageRollup,
|
|
245
|
+
eventSummary,
|
|
246
|
+
shouldUseDatabase
|
|
247
|
+
);
|
|
146
248
|
}
|
|
147
249
|
};
|
|
148
250
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve Migration Via Worker Use Case
|
|
3
|
+
*
|
|
4
|
+
* Resolves a failed Prisma migration (P3009) by invoking the worker Lambda,
|
|
5
|
+
* which has the Prisma CLI installed. Keeps the router Lambda lightweight —
|
|
6
|
+
* same delegation pattern as GetDatabaseStateViaWorkerUseCase.
|
|
7
|
+
*/
|
|
8
|
+
class ResolveMigrationViaWorkerUseCase {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} dependencies
|
|
11
|
+
* @param {LambdaInvoker} dependencies.lambdaInvoker - Lambda invocation adapter
|
|
12
|
+
* @param {string} dependencies.workerFunctionName - Worker Lambda function name
|
|
13
|
+
*/
|
|
14
|
+
constructor({ lambdaInvoker, workerFunctionName }) {
|
|
15
|
+
if (!lambdaInvoker) {
|
|
16
|
+
throw new Error('lambdaInvoker dependency is required');
|
|
17
|
+
}
|
|
18
|
+
if (!workerFunctionName) {
|
|
19
|
+
throw new Error('workerFunctionName is required');
|
|
20
|
+
}
|
|
21
|
+
this.lambdaInvoker = lambdaInvoker;
|
|
22
|
+
this.workerFunctionName = workerFunctionName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Object} params
|
|
27
|
+
* @param {string} params.migrationName - Migration to resolve
|
|
28
|
+
* @param {'applied'|'rolled-back'} [params.action] - Resolution mode
|
|
29
|
+
* @param {string} [params.stage] - Deployment stage
|
|
30
|
+
* @returns {Promise<Object>} Worker result body
|
|
31
|
+
*/
|
|
32
|
+
async execute({ migrationName, action = 'applied', stage }) {
|
|
33
|
+
const dbType = process.env.DB_TYPE || 'postgresql';
|
|
34
|
+
|
|
35
|
+
console.log(
|
|
36
|
+
`Invoking worker Lambda to resolve migration "${migrationName}" as ${action}: ${this.workerFunctionName}`
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return this.lambdaInvoker.invoke(this.workerFunctionName, {
|
|
40
|
+
action: 'resolve',
|
|
41
|
+
migrationName,
|
|
42
|
+
resolveAction: action,
|
|
43
|
+
dbType,
|
|
44
|
+
stage,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { ResolveMigrationViaWorkerUseCase };
|
|
@@ -405,14 +405,25 @@ async function runPrismaMigrateResolve(migrationName, action = 'applied', verbos
|
|
|
405
405
|
const [executable, ...executableArgs] = prismaBin.split(' ');
|
|
406
406
|
const fullArgs = [...executableArgs, ...args];
|
|
407
407
|
|
|
408
|
+
let stdout = '';
|
|
409
|
+
let stderr = '';
|
|
408
410
|
const proc = spawn(executable, fullArgs, {
|
|
409
|
-
stdio: 'inherit',
|
|
411
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
410
412
|
env: {
|
|
411
413
|
...process.env,
|
|
412
414
|
PRISMA_HIDE_UPDATE_MESSAGE: '1'
|
|
413
415
|
}
|
|
414
416
|
});
|
|
415
417
|
|
|
418
|
+
proc.stdout.on('data', (data) => {
|
|
419
|
+
stdout += data.toString();
|
|
420
|
+
if (verbose) process.stdout.write(data);
|
|
421
|
+
});
|
|
422
|
+
proc.stderr.on('data', (data) => {
|
|
423
|
+
stderr += data.toString();
|
|
424
|
+
if (verbose) process.stderr.write(data);
|
|
425
|
+
});
|
|
426
|
+
|
|
416
427
|
proc.on('error', (error) => {
|
|
417
428
|
resolve({
|
|
418
429
|
success: false,
|
|
@@ -427,9 +438,12 @@ async function runPrismaMigrateResolve(migrationName, action = 'applied', verbos
|
|
|
427
438
|
output: `Migration ${migrationName} marked as ${action}`
|
|
428
439
|
});
|
|
429
440
|
} else {
|
|
441
|
+
const detail = (stderr || stdout).trim();
|
|
430
442
|
resolve({
|
|
431
443
|
success: false,
|
|
432
|
-
error:
|
|
444
|
+
error: detail
|
|
445
|
+
? `Prisma migrate resolve failed (exit ${code}): ${detail}`
|
|
446
|
+
: `Resolve process exited with code ${code}`
|
|
433
447
|
});
|
|
434
448
|
}
|
|
435
449
|
});
|