@mevdragon/vidfarm-devcli 0.1.0
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/.env.example +41 -0
- package/AWS_REMOTION_HANDOFF.md +311 -0
- package/PLATFORM_SPEC.md +898 -0
- package/README.md +151 -0
- package/dist/src/app.js +224 -0
- package/dist/src/cli.js +187 -0
- package/dist/src/config.js +52 -0
- package/dist/src/context.js +198 -0
- package/dist/src/db.js +588 -0
- package/dist/src/dev-app.js +859 -0
- package/dist/src/domain.js +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/lib/crypto.js +30 -0
- package/dist/src/lib/ids.js +4 -0
- package/dist/src/lib/images.js +18 -0
- package/dist/src/lib/json.js +14 -0
- package/dist/src/lib/time.js +6 -0
- package/dist/src/registry.js +10 -0
- package/dist/src/runtime.js +19 -0
- package/dist/src/services/auth.js +80 -0
- package/dist/src/services/billing.js +16 -0
- package/dist/src/services/jobs.js +97 -0
- package/dist/src/services/providers.js +529 -0
- package/dist/src/services/remotion.js +158 -0
- package/dist/src/services/storage.js +93 -0
- package/dist/src/services/webhooks.js +61 -0
- package/dist/src/template-sdk.js +3 -0
- package/dist/src/worker.js +122 -0
- package/dist/templates/template_0000/demo-template.js +196 -0
- package/dist/templates/template_0000/remotion/Root.js +66 -0
- package/dist/templates/template_0000/remotion/index.js +3 -0
- package/package.json +59 -0
package/dist/src/db.js
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { config } from "./config.js";
|
|
3
|
+
import { parseJson, stringifyJson } from "./lib/json.js";
|
|
4
|
+
import { nowIso } from "./lib/time.js";
|
|
5
|
+
const db = new Database(config.VIDFARM_DB_PATH);
|
|
6
|
+
db.pragma("journal_mode = WAL");
|
|
7
|
+
db.pragma("foreign_keys = ON");
|
|
8
|
+
db.exec(`
|
|
9
|
+
create table if not exists customers (
|
|
10
|
+
id text primary key,
|
|
11
|
+
email text not null unique,
|
|
12
|
+
name text,
|
|
13
|
+
default_webhook_url text,
|
|
14
|
+
created_at text not null,
|
|
15
|
+
updated_at text not null
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
create table if not exists api_keys (
|
|
19
|
+
id text primary key,
|
|
20
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
21
|
+
key_hash text not null unique,
|
|
22
|
+
label text,
|
|
23
|
+
status text not null,
|
|
24
|
+
last_used_at text,
|
|
25
|
+
created_at text not null,
|
|
26
|
+
updated_at text not null
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
create table if not exists otp_challenges (
|
|
30
|
+
id text primary key,
|
|
31
|
+
email text not null,
|
|
32
|
+
code_hash text not null,
|
|
33
|
+
expires_at text not null,
|
|
34
|
+
consumed_at text,
|
|
35
|
+
created_at text not null
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
create table if not exists customer_provider_keys (
|
|
39
|
+
id text primary key,
|
|
40
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
41
|
+
provider text not null,
|
|
42
|
+
label text,
|
|
43
|
+
encrypted_secret text not null,
|
|
44
|
+
status text not null,
|
|
45
|
+
weight integer not null default 1,
|
|
46
|
+
last_used_at text,
|
|
47
|
+
cooldown_until text,
|
|
48
|
+
disabled_reason text,
|
|
49
|
+
created_at text not null,
|
|
50
|
+
updated_at text not null
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
create table if not exists provider_key_leases (
|
|
54
|
+
key_id text primary key references customer_provider_keys(id) on delete cascade,
|
|
55
|
+
lease_token text not null,
|
|
56
|
+
worker_id text not null,
|
|
57
|
+
job_id text not null,
|
|
58
|
+
leased_at text not null,
|
|
59
|
+
expires_at text not null
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
create table if not exists provider_key_usage_events (
|
|
63
|
+
id text primary key,
|
|
64
|
+
key_id text not null references customer_provider_keys(id) on delete cascade,
|
|
65
|
+
job_id text not null,
|
|
66
|
+
provider text not null,
|
|
67
|
+
model text,
|
|
68
|
+
event_type text not null,
|
|
69
|
+
input_tokens integer,
|
|
70
|
+
output_tokens integer,
|
|
71
|
+
cost_usd real,
|
|
72
|
+
metadata text,
|
|
73
|
+
created_at text not null
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
create table if not exists template_configs (
|
|
77
|
+
id text primary key,
|
|
78
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
79
|
+
template_id text not null,
|
|
80
|
+
config_json text not null,
|
|
81
|
+
created_at text not null,
|
|
82
|
+
updated_at text not null,
|
|
83
|
+
unique(customer_id, template_id)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
create table if not exists jobs (
|
|
87
|
+
id text primary key,
|
|
88
|
+
template_id text not null,
|
|
89
|
+
operation_name text not null,
|
|
90
|
+
workflow_name text not null,
|
|
91
|
+
tracer text not null,
|
|
92
|
+
status text not null,
|
|
93
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
94
|
+
payload_json text not null,
|
|
95
|
+
result_json text,
|
|
96
|
+
error_json text,
|
|
97
|
+
progress real not null default 0,
|
|
98
|
+
webhook_url text,
|
|
99
|
+
parent_job_id text references jobs(id) on delete set null,
|
|
100
|
+
priority integer not null default 100,
|
|
101
|
+
attempt_count integer not null default 0,
|
|
102
|
+
max_attempts integer not null default 6,
|
|
103
|
+
run_after text not null,
|
|
104
|
+
created_at text not null,
|
|
105
|
+
updated_at text not null,
|
|
106
|
+
started_at text,
|
|
107
|
+
completed_at text
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
create index if not exists idx_jobs_runnable on jobs(status, run_after, priority, created_at);
|
|
111
|
+
|
|
112
|
+
create table if not exists job_events (
|
|
113
|
+
id text primary key,
|
|
114
|
+
job_id text not null references jobs(id) on delete cascade,
|
|
115
|
+
level text not null,
|
|
116
|
+
message text not null,
|
|
117
|
+
metadata_json text not null,
|
|
118
|
+
progress real,
|
|
119
|
+
artifact_key text,
|
|
120
|
+
created_at text not null
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
create index if not exists idx_job_events_job_time on job_events(job_id, created_at);
|
|
124
|
+
|
|
125
|
+
create table if not exists artifacts (
|
|
126
|
+
id text primary key,
|
|
127
|
+
job_id text not null references jobs(id) on delete cascade,
|
|
128
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
129
|
+
template_id text not null,
|
|
130
|
+
kind text not null,
|
|
131
|
+
storage_key text not null,
|
|
132
|
+
public_url text,
|
|
133
|
+
metadata_json text not null,
|
|
134
|
+
created_at text not null
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
create table if not exists billing_events (
|
|
138
|
+
id text primary key,
|
|
139
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
140
|
+
job_id text not null references jobs(id) on delete cascade,
|
|
141
|
+
template_id text not null,
|
|
142
|
+
type text not null,
|
|
143
|
+
cost_usd real not null,
|
|
144
|
+
charge_usd real not null,
|
|
145
|
+
metadata_json text not null,
|
|
146
|
+
created_at text not null
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
create table if not exists webhook_deliveries (
|
|
150
|
+
id text primary key,
|
|
151
|
+
job_id text not null references jobs(id) on delete cascade,
|
|
152
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
153
|
+
event_type text not null,
|
|
154
|
+
destination_url text not null,
|
|
155
|
+
payload_json text not null,
|
|
156
|
+
status text not null,
|
|
157
|
+
attempt_count integer not null default 0,
|
|
158
|
+
next_attempt_at text not null,
|
|
159
|
+
last_error text,
|
|
160
|
+
created_at text not null,
|
|
161
|
+
updated_at text not null
|
|
162
|
+
);
|
|
163
|
+
`);
|
|
164
|
+
function mapJob(row) {
|
|
165
|
+
return {
|
|
166
|
+
id: String(row.id),
|
|
167
|
+
templateId: String(row.template_id),
|
|
168
|
+
operationName: String(row.operation_name),
|
|
169
|
+
workflowName: String(row.workflow_name),
|
|
170
|
+
tracer: String(row.tracer),
|
|
171
|
+
status: row.status,
|
|
172
|
+
customerId: String(row.customer_id),
|
|
173
|
+
payload: parseJson(row.payload_json, {}),
|
|
174
|
+
result: parseJson(row.result_json, null),
|
|
175
|
+
error: parseJson(row.error_json, null),
|
|
176
|
+
progress: Number(row.progress),
|
|
177
|
+
webhookUrl: row.webhook_url ? String(row.webhook_url) : null,
|
|
178
|
+
parentJobId: row.parent_job_id ? String(row.parent_job_id) : null,
|
|
179
|
+
priority: Number(row.priority),
|
|
180
|
+
attemptCount: Number(row.attempt_count),
|
|
181
|
+
maxAttempts: Number(row.max_attempts),
|
|
182
|
+
runAfter: String(row.run_after),
|
|
183
|
+
createdAt: String(row.created_at),
|
|
184
|
+
updatedAt: String(row.updated_at),
|
|
185
|
+
startedAt: row.started_at ? String(row.started_at) : null,
|
|
186
|
+
completedAt: row.completed_at ? String(row.completed_at) : null
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export const database = {
|
|
190
|
+
raw: db,
|
|
191
|
+
upsertCustomer(input) {
|
|
192
|
+
const timestamp = nowIso();
|
|
193
|
+
db.prepare(`
|
|
194
|
+
insert into customers (id, email, name, default_webhook_url, created_at, updated_at)
|
|
195
|
+
values (@id, @email, @name, @default_webhook_url, @created_at, @updated_at)
|
|
196
|
+
on conflict(id) do update set
|
|
197
|
+
email = excluded.email,
|
|
198
|
+
name = excluded.name,
|
|
199
|
+
default_webhook_url = excluded.default_webhook_url,
|
|
200
|
+
updated_at = excluded.updated_at
|
|
201
|
+
`).run({
|
|
202
|
+
id: input.id,
|
|
203
|
+
email: input.email,
|
|
204
|
+
name: input.name ?? null,
|
|
205
|
+
default_webhook_url: input.defaultWebhookUrl ?? null,
|
|
206
|
+
created_at: timestamp,
|
|
207
|
+
updated_at: timestamp
|
|
208
|
+
});
|
|
209
|
+
return this.getCustomerById(input.id);
|
|
210
|
+
},
|
|
211
|
+
getCustomerById(id) {
|
|
212
|
+
const row = db.prepare(`select * from customers where id = ?`).get(id);
|
|
213
|
+
if (!row) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
id: String(row.id),
|
|
218
|
+
email: String(row.email),
|
|
219
|
+
name: row.name ? String(row.name) : null,
|
|
220
|
+
defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
getCustomerByEmail(email) {
|
|
224
|
+
const row = db.prepare(`select * from customers where lower(email) = lower(?)`).get(email);
|
|
225
|
+
return row ? this.getCustomerById(String(row.id)) : null;
|
|
226
|
+
},
|
|
227
|
+
listProviderKeys(customerId) {
|
|
228
|
+
return db.prepare(`
|
|
229
|
+
select id, provider, label, status, weight, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
|
|
230
|
+
from customer_provider_keys
|
|
231
|
+
where customer_id = ?
|
|
232
|
+
order by provider asc, created_at desc
|
|
233
|
+
`).all(customerId);
|
|
234
|
+
},
|
|
235
|
+
insertOtpChallenge(record) {
|
|
236
|
+
db.prepare(`
|
|
237
|
+
insert into otp_challenges (id, email, code_hash, expires_at, created_at)
|
|
238
|
+
values (@id, @email, @code_hash, @expires_at, @created_at)
|
|
239
|
+
`).run({
|
|
240
|
+
id: record.id,
|
|
241
|
+
email: record.email,
|
|
242
|
+
code_hash: record.codeHash,
|
|
243
|
+
expires_at: record.expiresAt,
|
|
244
|
+
created_at: nowIso()
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
findLatestOtpChallenge(email) {
|
|
248
|
+
return db.prepare(`
|
|
249
|
+
select * from otp_challenges
|
|
250
|
+
where lower(email) = lower(?)
|
|
251
|
+
order by created_at desc
|
|
252
|
+
limit 1
|
|
253
|
+
`).get(email);
|
|
254
|
+
},
|
|
255
|
+
consumeOtpChallenge(id) {
|
|
256
|
+
db.prepare(`update otp_challenges set consumed_at = ? where id = ?`).run(nowIso(), id);
|
|
257
|
+
},
|
|
258
|
+
insertApiKey(record) {
|
|
259
|
+
const timestamp = nowIso();
|
|
260
|
+
db.prepare(`
|
|
261
|
+
insert into api_keys (id, customer_id, key_hash, label, status, created_at, updated_at)
|
|
262
|
+
values (@id, @customer_id, @key_hash, @label, 'active', @created_at, @updated_at)
|
|
263
|
+
`).run({
|
|
264
|
+
id: record.id,
|
|
265
|
+
customer_id: record.customerId,
|
|
266
|
+
key_hash: record.keyHash,
|
|
267
|
+
label: record.label,
|
|
268
|
+
created_at: timestamp,
|
|
269
|
+
updated_at: timestamp
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
findApiKeyByHash(keyHash) {
|
|
273
|
+
return db.prepare(`
|
|
274
|
+
select a.*, c.email, c.name, c.default_webhook_url
|
|
275
|
+
from api_keys a
|
|
276
|
+
join customers c on c.id = a.customer_id
|
|
277
|
+
where a.key_hash = ? and a.status = 'active'
|
|
278
|
+
`).get(keyHash);
|
|
279
|
+
},
|
|
280
|
+
touchApiKey(id) {
|
|
281
|
+
db.prepare(`update api_keys set last_used_at = ?, updated_at = ? where id = ?`).run(nowIso(), nowIso(), id);
|
|
282
|
+
},
|
|
283
|
+
upsertTemplateConfig(record) {
|
|
284
|
+
const timestamp = nowIso();
|
|
285
|
+
db.prepare(`
|
|
286
|
+
insert into template_configs (id, customer_id, template_id, config_json, created_at, updated_at)
|
|
287
|
+
values (@id, @customer_id, @template_id, @config_json, @created_at, @updated_at)
|
|
288
|
+
on conflict(customer_id, template_id) do update set
|
|
289
|
+
config_json = excluded.config_json,
|
|
290
|
+
updated_at = excluded.updated_at
|
|
291
|
+
`).run({
|
|
292
|
+
id: record.id,
|
|
293
|
+
customer_id: record.customerId,
|
|
294
|
+
template_id: record.templateId,
|
|
295
|
+
config_json: stringifyJson(record.config),
|
|
296
|
+
created_at: timestamp,
|
|
297
|
+
updated_at: timestamp
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
getTemplateConfig(customerId, templateId) {
|
|
301
|
+
const row = db.prepare(`
|
|
302
|
+
select config_json from template_configs
|
|
303
|
+
where customer_id = ? and template_id = ?
|
|
304
|
+
`).get(customerId, templateId);
|
|
305
|
+
return parseJson(row?.config_json, {});
|
|
306
|
+
},
|
|
307
|
+
createJob(record) {
|
|
308
|
+
const timestamp = nowIso();
|
|
309
|
+
db.prepare(`
|
|
310
|
+
insert into jobs (
|
|
311
|
+
id, template_id, operation_name, workflow_name, tracer, status, customer_id,
|
|
312
|
+
payload_json, result_json, error_json, progress, webhook_url, parent_job_id,
|
|
313
|
+
priority, attempt_count, max_attempts, run_after, created_at, updated_at, started_at, completed_at
|
|
314
|
+
)
|
|
315
|
+
values (
|
|
316
|
+
@id, @template_id, @operation_name, @workflow_name, @tracer, @status, @customer_id,
|
|
317
|
+
@payload_json, @result_json, @error_json, @progress, @webhook_url, @parent_job_id,
|
|
318
|
+
@priority, @attempt_count, @max_attempts, @run_after, @created_at, @updated_at, @started_at, @completed_at
|
|
319
|
+
)
|
|
320
|
+
`).run({
|
|
321
|
+
id: record.id,
|
|
322
|
+
template_id: record.templateId,
|
|
323
|
+
operation_name: record.operationName,
|
|
324
|
+
workflow_name: record.workflowName,
|
|
325
|
+
tracer: record.tracer,
|
|
326
|
+
status: record.status,
|
|
327
|
+
customer_id: record.customerId,
|
|
328
|
+
payload_json: stringifyJson(record.payload),
|
|
329
|
+
result_json: stringifyJson(record.result ?? null),
|
|
330
|
+
error_json: stringifyJson(record.error ?? null),
|
|
331
|
+
progress: record.progress,
|
|
332
|
+
webhook_url: record.webhookUrl,
|
|
333
|
+
parent_job_id: record.parentJobId,
|
|
334
|
+
priority: record.priority,
|
|
335
|
+
attempt_count: record.attemptCount,
|
|
336
|
+
max_attempts: record.maxAttempts,
|
|
337
|
+
run_after: record.runAfter,
|
|
338
|
+
created_at: timestamp,
|
|
339
|
+
updated_at: timestamp,
|
|
340
|
+
started_at: record.startedAt ?? null,
|
|
341
|
+
completed_at: record.completedAt ?? null
|
|
342
|
+
});
|
|
343
|
+
return this.getJob(record.id);
|
|
344
|
+
},
|
|
345
|
+
getJob(id) {
|
|
346
|
+
const row = db.prepare(`select * from jobs where id = ?`).get(id);
|
|
347
|
+
return row ? mapJob(row) : null;
|
|
348
|
+
},
|
|
349
|
+
listRunnableJobs(limit) {
|
|
350
|
+
const rows = db.prepare(`
|
|
351
|
+
select * from jobs
|
|
352
|
+
where status = 'queued' and run_after <= ?
|
|
353
|
+
order by priority asc, created_at asc
|
|
354
|
+
limit ?
|
|
355
|
+
`).all(nowIso(), limit);
|
|
356
|
+
return rows.map(mapJob);
|
|
357
|
+
},
|
|
358
|
+
markJobRunning(id) {
|
|
359
|
+
const timestamp = nowIso();
|
|
360
|
+
db.prepare(`
|
|
361
|
+
update jobs
|
|
362
|
+
set status = 'running', started_at = coalesce(started_at, ?), updated_at = ?, attempt_count = attempt_count + 1
|
|
363
|
+
where id = ?
|
|
364
|
+
`).run(timestamp, timestamp, id);
|
|
365
|
+
},
|
|
366
|
+
updateJobStatus(input) {
|
|
367
|
+
const current = this.getJob(input.id);
|
|
368
|
+
db.prepare(`
|
|
369
|
+
update jobs
|
|
370
|
+
set status = @status,
|
|
371
|
+
progress = @progress,
|
|
372
|
+
result_json = @result_json,
|
|
373
|
+
error_json = @error_json,
|
|
374
|
+
run_after = @run_after,
|
|
375
|
+
completed_at = @completed_at,
|
|
376
|
+
updated_at = @updated_at
|
|
377
|
+
where id = @id
|
|
378
|
+
`).run({
|
|
379
|
+
id: input.id,
|
|
380
|
+
status: input.status,
|
|
381
|
+
progress: input.progress ?? current?.progress ?? 0,
|
|
382
|
+
result_json: input.result === undefined ? stringifyJson(current?.result ?? null) : stringifyJson(input.result),
|
|
383
|
+
error_json: input.error === undefined ? stringifyJson(current?.error ?? null) : stringifyJson(input.error),
|
|
384
|
+
run_after: input.runAfter ?? current?.runAfter ?? nowIso(),
|
|
385
|
+
completed_at: input.completedAt ?? current?.completedAt ?? null,
|
|
386
|
+
updated_at: nowIso()
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
listJobsForCustomer(customerId, templateId) {
|
|
390
|
+
const rows = templateId
|
|
391
|
+
? db.prepare(`select * from jobs where customer_id = ? and template_id = ? order by created_at desc limit 100`).all(customerId, templateId)
|
|
392
|
+
: db.prepare(`select * from jobs where customer_id = ? order by created_at desc limit 100`).all(customerId);
|
|
393
|
+
return rows.map(mapJob);
|
|
394
|
+
},
|
|
395
|
+
addJobEvent(event) {
|
|
396
|
+
db.prepare(`
|
|
397
|
+
insert into job_events (id, job_id, level, message, metadata_json, progress, artifact_key, created_at)
|
|
398
|
+
values (@id, @job_id, @level, @message, @metadata_json, @progress, @artifact_key, @created_at)
|
|
399
|
+
`).run({
|
|
400
|
+
id: event.id,
|
|
401
|
+
job_id: event.jobId,
|
|
402
|
+
level: event.level,
|
|
403
|
+
message: event.message,
|
|
404
|
+
metadata_json: stringifyJson(event.metadata),
|
|
405
|
+
progress: event.progress,
|
|
406
|
+
artifact_key: event.artifactKey,
|
|
407
|
+
created_at: nowIso()
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
getJobEvents(jobId, since, limit = 100) {
|
|
411
|
+
const rows = since
|
|
412
|
+
? db.prepare(`
|
|
413
|
+
select * from job_events where job_id = ? and created_at > ? order by created_at asc limit ?
|
|
414
|
+
`).all(jobId, since, limit)
|
|
415
|
+
: db.prepare(`
|
|
416
|
+
select * from job_events where job_id = ? order by created_at asc limit ?
|
|
417
|
+
`).all(jobId, limit);
|
|
418
|
+
return rows.map((row) => ({
|
|
419
|
+
id: String(row.id),
|
|
420
|
+
jobId: String(row.job_id),
|
|
421
|
+
level: row.level,
|
|
422
|
+
message: String(row.message),
|
|
423
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
424
|
+
progress: row.progress === null ? null : Number(row.progress),
|
|
425
|
+
artifactKey: row.artifact_key ? String(row.artifact_key) : null,
|
|
426
|
+
createdAt: String(row.created_at)
|
|
427
|
+
}));
|
|
428
|
+
},
|
|
429
|
+
insertArtifact(record) {
|
|
430
|
+
db.prepare(`
|
|
431
|
+
insert into artifacts (id, job_id, customer_id, template_id, kind, storage_key, public_url, metadata_json, created_at)
|
|
432
|
+
values (@id, @job_id, @customer_id, @template_id, @kind, @storage_key, @public_url, @metadata_json, @created_at)
|
|
433
|
+
`).run({
|
|
434
|
+
id: record.id,
|
|
435
|
+
job_id: record.jobId,
|
|
436
|
+
customer_id: record.customerId,
|
|
437
|
+
template_id: record.templateId,
|
|
438
|
+
kind: record.kind,
|
|
439
|
+
storage_key: record.storageKey,
|
|
440
|
+
public_url: record.publicUrl ?? null,
|
|
441
|
+
metadata_json: stringifyJson(record.metadata ?? {}),
|
|
442
|
+
created_at: nowIso()
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
insertBillingEvent(record) {
|
|
446
|
+
db.prepare(`
|
|
447
|
+
insert into billing_events (id, customer_id, job_id, template_id, type, cost_usd, charge_usd, metadata_json, created_at)
|
|
448
|
+
values (@id, @customer_id, @job_id, @template_id, @type, @cost_usd, @charge_usd, @metadata_json, @created_at)
|
|
449
|
+
`).run({
|
|
450
|
+
id: record.id,
|
|
451
|
+
customer_id: record.customerId,
|
|
452
|
+
job_id: record.jobId,
|
|
453
|
+
template_id: record.templateId,
|
|
454
|
+
type: record.type,
|
|
455
|
+
cost_usd: record.costUsd,
|
|
456
|
+
charge_usd: record.chargeUsd,
|
|
457
|
+
metadata_json: stringifyJson(record.metadata ?? {}),
|
|
458
|
+
created_at: nowIso()
|
|
459
|
+
});
|
|
460
|
+
},
|
|
461
|
+
queueWebhookDelivery(record) {
|
|
462
|
+
const timestamp = nowIso();
|
|
463
|
+
db.prepare(`
|
|
464
|
+
insert into webhook_deliveries (
|
|
465
|
+
id, job_id, customer_id, event_type, destination_url, payload_json, status,
|
|
466
|
+
attempt_count, next_attempt_at, created_at, updated_at
|
|
467
|
+
) values (
|
|
468
|
+
@id, @job_id, @customer_id, @event_type, @destination_url, @payload_json, 'pending',
|
|
469
|
+
0, @next_attempt_at, @created_at, @updated_at
|
|
470
|
+
)
|
|
471
|
+
`).run({
|
|
472
|
+
id: record.id,
|
|
473
|
+
job_id: record.jobId,
|
|
474
|
+
customer_id: record.customerId,
|
|
475
|
+
event_type: record.eventType,
|
|
476
|
+
destination_url: record.destinationUrl,
|
|
477
|
+
payload_json: stringifyJson(record.payload),
|
|
478
|
+
next_attempt_at: timestamp,
|
|
479
|
+
created_at: timestamp,
|
|
480
|
+
updated_at: timestamp
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
listPendingWebhookDeliveries(limit) {
|
|
484
|
+
return db.prepare(`
|
|
485
|
+
select * from webhook_deliveries
|
|
486
|
+
where status in ('pending', 'retrying') and next_attempt_at <= ?
|
|
487
|
+
order by next_attempt_at asc
|
|
488
|
+
limit ?
|
|
489
|
+
`).all(nowIso(), limit);
|
|
490
|
+
},
|
|
491
|
+
markWebhookDelivery(record) {
|
|
492
|
+
db.prepare(`
|
|
493
|
+
update webhook_deliveries
|
|
494
|
+
set status = @status,
|
|
495
|
+
attempt_count = @attempt_count,
|
|
496
|
+
next_attempt_at = coalesce(@next_attempt_at, next_attempt_at),
|
|
497
|
+
last_error = @last_error,
|
|
498
|
+
updated_at = @updated_at
|
|
499
|
+
where id = @id
|
|
500
|
+
`).run({
|
|
501
|
+
id: record.id,
|
|
502
|
+
status: record.status,
|
|
503
|
+
attempt_count: record.attemptCount,
|
|
504
|
+
next_attempt_at: record.nextAttemptAt ?? null,
|
|
505
|
+
last_error: record.lastError ?? null,
|
|
506
|
+
updated_at: nowIso()
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
acquireProviderKeyLease(input) {
|
|
510
|
+
const transaction = db.transaction(() => {
|
|
511
|
+
db.prepare(`delete from provider_key_leases where expires_at <= ?`).run(nowIso());
|
|
512
|
+
const row = db.prepare(`
|
|
513
|
+
select k.id, k.encrypted_secret
|
|
514
|
+
from customer_provider_keys k
|
|
515
|
+
left join provider_key_leases l on l.key_id = k.id and l.expires_at > ?
|
|
516
|
+
where k.customer_id = ?
|
|
517
|
+
and k.provider = ?
|
|
518
|
+
and k.status = 'active'
|
|
519
|
+
and (k.cooldown_until is null or k.cooldown_until <= ?)
|
|
520
|
+
and l.key_id is null
|
|
521
|
+
order by case when k.last_used_at is null then 0 else 1 end asc, k.last_used_at asc, k.weight desc
|
|
522
|
+
limit 1
|
|
523
|
+
`).get(nowIso(), input.customerId, input.provider, nowIso());
|
|
524
|
+
if (!row) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
db.prepare(`
|
|
528
|
+
insert into provider_key_leases (key_id, lease_token, worker_id, job_id, leased_at, expires_at)
|
|
529
|
+
values (?, ?, ?, ?, ?, ?)
|
|
530
|
+
`).run(row.id, input.leaseToken, input.workerId, input.jobId, nowIso(), input.expiresAt);
|
|
531
|
+
return { keyId: row.id, encryptedSecret: row.encrypted_secret };
|
|
532
|
+
});
|
|
533
|
+
return transaction();
|
|
534
|
+
},
|
|
535
|
+
releaseProviderKeyLease(input) {
|
|
536
|
+
db.prepare(`delete from provider_key_leases where key_id = ? and lease_token = ?`).run(input.keyId, input.leaseToken);
|
|
537
|
+
},
|
|
538
|
+
recordProviderKeyUsage(input) {
|
|
539
|
+
db.prepare(`
|
|
540
|
+
insert into provider_key_usage_events (
|
|
541
|
+
id, key_id, job_id, provider, model, event_type, input_tokens, output_tokens, cost_usd, metadata, created_at
|
|
542
|
+
) values (
|
|
543
|
+
@id, @key_id, @job_id, @provider, @model, @event_type, @input_tokens, @output_tokens, @cost_usd, @metadata, @created_at
|
|
544
|
+
)
|
|
545
|
+
`).run({
|
|
546
|
+
id: input.id,
|
|
547
|
+
key_id: input.keyId,
|
|
548
|
+
job_id: input.jobId,
|
|
549
|
+
provider: input.provider,
|
|
550
|
+
model: input.model ?? null,
|
|
551
|
+
event_type: input.eventType,
|
|
552
|
+
input_tokens: input.inputTokens ?? null,
|
|
553
|
+
output_tokens: input.outputTokens ?? null,
|
|
554
|
+
cost_usd: input.costUsd ?? null,
|
|
555
|
+
metadata: stringifyJson(input.metadata ?? {}),
|
|
556
|
+
created_at: nowIso()
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
touchProviderKey(keyId) {
|
|
560
|
+
db.prepare(`update customer_provider_keys set last_used_at = ?, updated_at = ? where id = ?`).run(nowIso(), nowIso(), keyId);
|
|
561
|
+
},
|
|
562
|
+
setProviderKeyCooldown(keyId, cooldownUntil, status = "active", disabledReason = null) {
|
|
563
|
+
db.prepare(`
|
|
564
|
+
update customer_provider_keys
|
|
565
|
+
set cooldown_until = ?, status = ?, disabled_reason = ?, updated_at = ?
|
|
566
|
+
where id = ?
|
|
567
|
+
`).run(cooldownUntil, status, disabledReason, nowIso(), keyId);
|
|
568
|
+
},
|
|
569
|
+
createProviderKey(record) {
|
|
570
|
+
const timestamp = nowIso();
|
|
571
|
+
db.prepare(`
|
|
572
|
+
insert into customer_provider_keys (
|
|
573
|
+
id, customer_id, provider, label, encrypted_secret, status, weight, created_at, updated_at
|
|
574
|
+
) values (
|
|
575
|
+
@id, @customer_id, @provider, @label, @encrypted_secret, 'active', @weight, @created_at, @updated_at
|
|
576
|
+
)
|
|
577
|
+
`).run({
|
|
578
|
+
id: record.id,
|
|
579
|
+
customer_id: record.customerId,
|
|
580
|
+
provider: record.provider,
|
|
581
|
+
label: record.label,
|
|
582
|
+
encrypted_secret: record.encryptedSecret,
|
|
583
|
+
weight: record.weight,
|
|
584
|
+
created_at: timestamp,
|
|
585
|
+
updated_at: timestamp
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
};
|