@mevdragon/vidfarm-devcli 0.2.8 → 0.2.10
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 +9 -0
- package/GETTING_STARTED.developers.md +1 -26
- package/README.md +3 -0
- package/SKILL.developer.md +419 -8
- package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +475 -0
- package/auto-create-templates/extractor-system-prompt.md +113 -0
- package/auto-create-templates/production-graph.schema.json +276 -0
- package/auto-create-templates/runbook.md +140 -0
- package/auto-create-templates/template-plan.schema.json +230 -0
- package/auto-create-templates/template-planner-prompt.md +75 -0
- package/dist/src/account-pages-legacy.js +9064 -0
- package/dist/src/account-pages.js +41 -620
- package/dist/src/app.js +6627 -244
- package/dist/src/cli.js +386 -26
- package/dist/src/config.js +12 -2
- package/dist/src/context.js +35 -35
- package/dist/src/db.js +1016 -53
- package/dist/src/dev-app-legacy.js +693 -0
- package/dist/src/dev-app.js +4 -904
- package/dist/src/domain.js +1 -1
- package/dist/src/editor-chat-history.js +72 -0
- package/dist/src/editor-chat.js +202 -0
- package/dist/src/frontend/flockposter-cache-store.js +116 -0
- package/dist/src/frontend/homepage-client.js +114 -0
- package/dist/src/frontend/homepage-shared.js +3 -0
- package/dist/src/frontend/homepage-store.js +27 -0
- package/dist/src/frontend/homepage-view.js +147 -0
- package/dist/src/frontend/page-runtime-client.js +100 -0
- package/dist/src/frontend/page-runtime-store.js +8 -0
- package/dist/src/frontend/template-editor-chat.js +2261 -0
- package/dist/src/homepage.js +695 -371
- package/dist/src/lib/template-style-options.js +95 -15
- package/dist/src/page-runtime.js +1 -0
- package/dist/src/page-shell.js +941 -0
- package/dist/src/primitive-context.js +163 -0
- package/dist/src/primitive-registry.js +340 -0
- package/dist/src/primitive-sdk.js +3 -0
- package/dist/src/primitives/remotion/html-image.js +28 -0
- package/dist/src/react-page-shell.js +34 -0
- package/dist/src/ready-post-schedule-component.js +1514 -0
- package/dist/src/registry.js +36 -17
- package/dist/src/runtime.js +6 -1
- package/dist/src/services/api-call-history.js +245 -0
- package/dist/src/services/auth.js +25 -3
- package/dist/src/services/billing.js +248 -9
- package/dist/src/services/chat-threads.js +88 -0
- package/dist/src/services/job-logs.js +14 -7
- package/dist/src/services/jobs.js +13 -2
- package/dist/src/services/providers.js +552 -133
- package/dist/src/services/rate-limits.js +236 -0
- package/dist/src/services/remotion.js +143 -16
- package/dist/src/services/storage.js +10 -8
- package/dist/src/services/template-certification.js +11 -2
- package/dist/src/services/template-loader.js +3 -1
- package/dist/src/services/template-sources.js +68 -3
- package/dist/src/template-editor-pages.js +2309 -0
- package/dist/src/template-editor-shell.js +1507 -0
- package/dist/src/worker.js +120 -22
- package/package.json +27 -4
- package/public/assets/homepage-app.js +54 -0
- package/public/assets/homepage-client-app.js +54 -0
- package/public/assets/page-runtime-client-app.js +68 -0
- package/dist/templates/vidfarm_template_0000/src/lib/images.js +0 -202
- package/dist/templates/vidfarm_template_0000/src/remotion/Root.js +0 -34
- package/dist/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
- package/dist/templates/vidfarm_template_0000/src/sdk.js +0 -3
- package/dist/templates/vidfarm_template_0000/src/style-options.js +0 -51
- package/dist/templates/vidfarm_template_0000/src/template-dna.js +0 -9
- package/dist/templates/vidfarm_template_0000/src/template.js +0 -1447
- package/templates/vidfarm_template_0000/README.md +0 -100
- package/templates/vidfarm_template_0000/SKILL.md +0 -225
- package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/composition.json +0 -11
- package/templates/vidfarm_template_0000/package.json +0 -27
- package/templates/vidfarm_template_0000/research/preview/.gitkeep +0 -1
- package/templates/vidfarm_template_0000/research/source_notes.md +0 -7
- package/templates/vidfarm_template_0000/src/lib/images.js +0 -202
- package/templates/vidfarm_template_0000/src/lib/images.ts +0 -241
- package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
- package/templates/vidfarm_template_0000/src/remotion/Root.tsx +0 -75
- package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
- package/templates/vidfarm_template_0000/src/remotion/index.tsx +0 -4
- package/templates/vidfarm_template_0000/src/sdk.js +0 -3
- package/templates/vidfarm_template_0000/src/sdk.ts +0 -140
- package/templates/vidfarm_template_0000/src/style-options.js +0 -51
- package/templates/vidfarm_template_0000/src/style-options.ts +0 -60
- package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
- package/templates/vidfarm_template_0000/src/template-dna.ts +0 -15
- package/templates/vidfarm_template_0000/src/template.js +0 -1447
- package/templates/vidfarm_template_0000/src/template.ts +0 -2049
- package/templates/vidfarm_template_0000/template.config.json +0 -21
- package/templates/vidfarm_template_0000/tmp/solobacterium-moorei-slideshow.request.json +0 -31
- package/templates/vidfarm_template_0000/tsconfig.json +0 -19
package/dist/src/db.js
CHANGED
|
@@ -14,6 +14,7 @@ create table if not exists customers (
|
|
|
14
14
|
default_webhook_url text,
|
|
15
15
|
is_developer integer not null default 0,
|
|
16
16
|
is_paid_plan integer not null default 0,
|
|
17
|
+
plan_tier text not null default 'starter',
|
|
17
18
|
about text,
|
|
18
19
|
groupchat_url text,
|
|
19
20
|
flockposter_api_key text,
|
|
@@ -50,7 +51,6 @@ create table if not exists customer_provider_keys (
|
|
|
50
51
|
label text,
|
|
51
52
|
encrypted_secret text not null,
|
|
52
53
|
status text not null,
|
|
53
|
-
weight integer not null default 1,
|
|
54
54
|
last_used_at text,
|
|
55
55
|
cooldown_until text,
|
|
56
56
|
disabled_reason text,
|
|
@@ -67,6 +67,19 @@ create table if not exists provider_key_leases (
|
|
|
67
67
|
expires_at text not null
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
+
create table if not exists provider_key_waiters (
|
|
71
|
+
id text primary key,
|
|
72
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
73
|
+
provider text not null,
|
|
74
|
+
worker_id text not null,
|
|
75
|
+
owner_id text not null,
|
|
76
|
+
priority integer not null default 100,
|
|
77
|
+
created_at text not null,
|
|
78
|
+
expires_at text not null
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
create index if not exists idx_provider_key_waiters_scope on provider_key_waiters(customer_id, provider, priority, created_at);
|
|
82
|
+
|
|
70
83
|
create table if not exists provider_key_usage_events (
|
|
71
84
|
id text primary key,
|
|
72
85
|
key_id text not null references customer_provider_keys(id) on delete cascade,
|
|
@@ -129,18 +142,20 @@ create table if not exists artifacts (
|
|
|
129
142
|
created_at text not null
|
|
130
143
|
);
|
|
131
144
|
|
|
132
|
-
|
|
145
|
+
drop table if exists billing_events;
|
|
146
|
+
|
|
147
|
+
create table if not exists customer_credit_grants (
|
|
133
148
|
id text primary key,
|
|
134
149
|
customer_id text not null references customers(id) on delete cascade,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
type text not null,
|
|
138
|
-
cost_usd real not null,
|
|
139
|
-
charge_usd real not null,
|
|
150
|
+
grant_type text not null,
|
|
151
|
+
amount_usd real not null,
|
|
140
152
|
metadata_json text not null,
|
|
141
|
-
created_at text not null
|
|
153
|
+
created_at text not null,
|
|
154
|
+
unique(customer_id, grant_type)
|
|
142
155
|
);
|
|
143
156
|
|
|
157
|
+
create index if not exists idx_customer_credit_grants_customer_created on customer_credit_grants(customer_id, created_at desc);
|
|
158
|
+
|
|
144
159
|
create table if not exists webhook_deliveries (
|
|
145
160
|
id text primary key,
|
|
146
161
|
job_id text not null references jobs(id) on delete cascade,
|
|
@@ -169,6 +184,110 @@ create table if not exists user_attachments (
|
|
|
169
184
|
|
|
170
185
|
create index if not exists idx_user_attachments_customer_created on user_attachments(customer_id, created_at desc);
|
|
171
186
|
|
|
187
|
+
create table if not exists email_channels (
|
|
188
|
+
id text primary key,
|
|
189
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
190
|
+
title text not null,
|
|
191
|
+
email text not null,
|
|
192
|
+
note text,
|
|
193
|
+
avatar_url text,
|
|
194
|
+
avatar_storage_key text,
|
|
195
|
+
status text not null,
|
|
196
|
+
verification_token_hash text,
|
|
197
|
+
verification_sent_at text,
|
|
198
|
+
verified_at text,
|
|
199
|
+
created_at text not null,
|
|
200
|
+
updated_at text not null,
|
|
201
|
+
unique(customer_id, email)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
create index if not exists idx_email_channels_customer_created on email_channels(customer_id, created_at desc);
|
|
205
|
+
create index if not exists idx_email_channels_token on email_channels(verification_token_hash);
|
|
206
|
+
|
|
207
|
+
create table if not exists ready_posts (
|
|
208
|
+
id text primary key,
|
|
209
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
210
|
+
status text not null,
|
|
211
|
+
tracer text,
|
|
212
|
+
title text,
|
|
213
|
+
content text not null,
|
|
214
|
+
pinned_comment text,
|
|
215
|
+
scheduled_at text,
|
|
216
|
+
timezone text,
|
|
217
|
+
post_type text not null,
|
|
218
|
+
integration_ids_json text not null,
|
|
219
|
+
platform_targets_json text not null,
|
|
220
|
+
media_assets_json text not null,
|
|
221
|
+
renditions_json text not null,
|
|
222
|
+
flockposter_payload_json text not null,
|
|
223
|
+
field_values_json text not null,
|
|
224
|
+
share_password_hash text,
|
|
225
|
+
zip_storage_key text,
|
|
226
|
+
zip_public_url text,
|
|
227
|
+
archived_at text,
|
|
228
|
+
deleted_at text,
|
|
229
|
+
created_at text not null,
|
|
230
|
+
updated_at text not null
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
create index if not exists idx_ready_posts_customer_created on ready_posts(customer_id, created_at desc);
|
|
234
|
+
|
|
235
|
+
create table if not exists post_schedules (
|
|
236
|
+
id text primary key,
|
|
237
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
238
|
+
ready_post_id text references ready_posts(id) on delete cascade,
|
|
239
|
+
managed_by text not null,
|
|
240
|
+
provider text not null,
|
|
241
|
+
destination_type text not null,
|
|
242
|
+
destination_id text not null,
|
|
243
|
+
scheduled_at text not null,
|
|
244
|
+
timezone text,
|
|
245
|
+
status text not null,
|
|
246
|
+
provider_status text,
|
|
247
|
+
provider_schedule_id text,
|
|
248
|
+
title text,
|
|
249
|
+
summary text,
|
|
250
|
+
url text,
|
|
251
|
+
additional_notes text,
|
|
252
|
+
provider_payload_json text not null,
|
|
253
|
+
provider_response_json text not null,
|
|
254
|
+
last_synced_at text,
|
|
255
|
+
last_error text,
|
|
256
|
+
cancelled_at text,
|
|
257
|
+
sent_at text,
|
|
258
|
+
created_at text not null,
|
|
259
|
+
updated_at text not null
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
create index if not exists idx_post_schedules_customer_scheduled on post_schedules(customer_id, scheduled_at asc);
|
|
263
|
+
create index if not exists idx_post_schedules_ready_post on post_schedules(ready_post_id, status);
|
|
264
|
+
create index if not exists idx_post_schedules_provider_id on post_schedules(customer_id, provider, provider_schedule_id);
|
|
265
|
+
|
|
266
|
+
create table if not exists chat_threads (
|
|
267
|
+
id text primary key,
|
|
268
|
+
customer_id text not null references customers(id) on delete cascade,
|
|
269
|
+
template_id text not null,
|
|
270
|
+
title text not null,
|
|
271
|
+
storage_prefix text not null,
|
|
272
|
+
message_count integer not null default 0,
|
|
273
|
+
last_message_at text,
|
|
274
|
+
hidden_from_view integer not null default 0,
|
|
275
|
+
archived_at text,
|
|
276
|
+
created_at text not null,
|
|
277
|
+
updated_at text not null
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
create index if not exists idx_chat_threads_customer_template_updated on chat_threads(customer_id, template_id, updated_at desc);
|
|
281
|
+
|
|
282
|
+
create table if not exists chat_thread_tracers (
|
|
283
|
+
thread_id text not null references chat_threads(id) on delete cascade,
|
|
284
|
+
tracer text not null,
|
|
285
|
+
created_at text not null,
|
|
286
|
+
primary key (thread_id, tracer)
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
create index if not exists idx_chat_thread_tracers_tracer on chat_thread_tracers(tracer, thread_id);
|
|
290
|
+
|
|
172
291
|
create table if not exists template_sources (
|
|
173
292
|
id text primary key,
|
|
174
293
|
template_id text not null unique,
|
|
@@ -212,6 +331,16 @@ if (!customerColumns.some((column) => column.name === "is_developer")) {
|
|
|
212
331
|
if (!customerColumns.some((column) => column.name === "is_paid_plan")) {
|
|
213
332
|
db.exec(`alter table customers add column is_paid_plan integer not null default 0;`);
|
|
214
333
|
}
|
|
334
|
+
if (!customerColumns.some((column) => column.name === "plan_tier")) {
|
|
335
|
+
db.exec(`alter table customers add column plan_tier text not null default 'starter';`);
|
|
336
|
+
}
|
|
337
|
+
db.exec(`
|
|
338
|
+
update customers
|
|
339
|
+
set plan_tier = 'starter'
|
|
340
|
+
where plan_tier in ('standard', 'invisible')
|
|
341
|
+
or plan_tier is null
|
|
342
|
+
or trim(plan_tier) = ''
|
|
343
|
+
`);
|
|
215
344
|
if (!customerColumns.some((column) => column.name === "password_hash")) {
|
|
216
345
|
db.exec(`alter table customers add column password_hash text;`);
|
|
217
346
|
}
|
|
@@ -228,6 +357,10 @@ const apiKeyColumns = db.prepare(`pragma table_info(api_keys)`).all();
|
|
|
228
357
|
if (!apiKeyColumns.some((column) => column.name === "raw_value")) {
|
|
229
358
|
db.exec(`alter table api_keys add column raw_value text;`);
|
|
230
359
|
}
|
|
360
|
+
const providerKeyColumns = db.prepare(`pragma table_info(customer_provider_keys)`).all();
|
|
361
|
+
if (providerKeyColumns.some((column) => column.name === "weight")) {
|
|
362
|
+
db.exec(`alter table customer_provider_keys drop column weight;`);
|
|
363
|
+
}
|
|
231
364
|
const templateSourceColumns = db.prepare(`pragma table_info(template_sources)`).all();
|
|
232
365
|
if (!templateSourceColumns.some((column) => column.name === "slug_id")) {
|
|
233
366
|
db.exec(`alter table template_sources add column slug_id text;`);
|
|
@@ -255,6 +388,24 @@ if (!templateReleaseColumns.some((column) => column.name === "artifact_manifest_
|
|
|
255
388
|
if (!templateReleaseColumns.some((column) => column.name === "artifact_key")) {
|
|
256
389
|
db.exec(`alter table template_releases add column artifact_key text;`);
|
|
257
390
|
}
|
|
391
|
+
const readyPostColumns = db.prepare(`pragma table_info(ready_posts)`).all();
|
|
392
|
+
if (!readyPostColumns.some((column) => column.name === "tracer")) {
|
|
393
|
+
db.exec(`alter table ready_posts add column tracer text;`);
|
|
394
|
+
}
|
|
395
|
+
if (!readyPostColumns.some((column) => column.name === "pinned_comment")) {
|
|
396
|
+
db.exec(`alter table ready_posts add column pinned_comment text;`);
|
|
397
|
+
}
|
|
398
|
+
const postScheduleColumns = db.prepare(`pragma table_info(post_schedules)`).all();
|
|
399
|
+
if (!postScheduleColumns.some((column) => column.name === "additional_notes")) {
|
|
400
|
+
db.exec(`alter table post_schedules add column additional_notes text;`);
|
|
401
|
+
}
|
|
402
|
+
const emailChannelColumns = db.prepare(`pragma table_info(email_channels)`).all();
|
|
403
|
+
if (!emailChannelColumns.some((column) => column.name === "avatar_url")) {
|
|
404
|
+
db.exec(`alter table email_channels add column avatar_url text;`);
|
|
405
|
+
}
|
|
406
|
+
if (!emailChannelColumns.some((column) => column.name === "avatar_storage_key")) {
|
|
407
|
+
db.exec(`alter table email_channels add column avatar_storage_key text;`);
|
|
408
|
+
}
|
|
258
409
|
db.exec(`drop index if exists idx_job_events_job_time;`);
|
|
259
410
|
db.exec(`drop table if exists job_events;`);
|
|
260
411
|
function mapJob(row) {
|
|
@@ -329,6 +480,101 @@ function mapUserAttachment(row) {
|
|
|
329
480
|
createdAt: String(row.created_at)
|
|
330
481
|
};
|
|
331
482
|
}
|
|
483
|
+
function mapEmailChannel(row) {
|
|
484
|
+
const status = String(row.status) === "verified" ? "verified" : "unverified";
|
|
485
|
+
return {
|
|
486
|
+
id: String(row.id),
|
|
487
|
+
customerId: String(row.customer_id),
|
|
488
|
+
title: String(row.title),
|
|
489
|
+
email: String(row.email),
|
|
490
|
+
note: row.note ? String(row.note) : null,
|
|
491
|
+
avatarUrl: row.avatar_url ? String(row.avatar_url) : null,
|
|
492
|
+
avatarStorageKey: row.avatar_storage_key ? String(row.avatar_storage_key) : null,
|
|
493
|
+
status,
|
|
494
|
+
verificationTokenHash: row.verification_token_hash ? String(row.verification_token_hash) : null,
|
|
495
|
+
verificationSentAt: row.verification_sent_at ? String(row.verification_sent_at) : null,
|
|
496
|
+
verifiedAt: row.verified_at ? String(row.verified_at) : null,
|
|
497
|
+
createdAt: String(row.created_at),
|
|
498
|
+
updatedAt: String(row.updated_at)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function mapArtifact(row) {
|
|
502
|
+
return {
|
|
503
|
+
id: String(row.id),
|
|
504
|
+
jobId: String(row.job_id),
|
|
505
|
+
customerId: String(row.customer_id),
|
|
506
|
+
templateId: String(row.template_id),
|
|
507
|
+
kind: String(row.kind),
|
|
508
|
+
storageKey: String(row.storage_key),
|
|
509
|
+
publicUrl: row.public_url ? String(row.public_url) : null,
|
|
510
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
511
|
+
createdAt: String(row.created_at)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function mapReadyPost(row) {
|
|
515
|
+
return {
|
|
516
|
+
id: String(row.id),
|
|
517
|
+
customerId: String(row.customer_id),
|
|
518
|
+
status: row.status,
|
|
519
|
+
tracer: row.tracer ? String(row.tracer) : null,
|
|
520
|
+
title: row.title ? String(row.title) : null,
|
|
521
|
+
caption: String(row.content),
|
|
522
|
+
pinnedComment: row.pinned_comment ? String(row.pinned_comment) : null,
|
|
523
|
+
mediaAssets: parseJson(row.media_assets_json, []),
|
|
524
|
+
sharePasswordHash: row.share_password_hash ? String(row.share_password_hash) : null,
|
|
525
|
+
zipStorageKey: row.zip_storage_key ? String(row.zip_storage_key) : null,
|
|
526
|
+
zipPublicUrl: row.zip_public_url ? String(row.zip_public_url) : null,
|
|
527
|
+
archivedAt: row.archived_at ? String(row.archived_at) : null,
|
|
528
|
+
deletedAt: row.deleted_at ? String(row.deleted_at) : null,
|
|
529
|
+
createdAt: String(row.created_at),
|
|
530
|
+
updatedAt: String(row.updated_at)
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function mapPostSchedule(row) {
|
|
534
|
+
return {
|
|
535
|
+
id: String(row.id),
|
|
536
|
+
customerId: String(row.customer_id),
|
|
537
|
+
readyPostId: row.ready_post_id ? String(row.ready_post_id) : null,
|
|
538
|
+
managedBy: row.managed_by,
|
|
539
|
+
provider: row.provider,
|
|
540
|
+
destinationType: row.destination_type,
|
|
541
|
+
destinationId: String(row.destination_id),
|
|
542
|
+
scheduledAt: String(row.scheduled_at),
|
|
543
|
+
timezone: row.timezone ? String(row.timezone) : null,
|
|
544
|
+
status: row.status,
|
|
545
|
+
providerStatus: row.provider_status ? String(row.provider_status) : null,
|
|
546
|
+
providerScheduleId: row.provider_schedule_id ? String(row.provider_schedule_id) : null,
|
|
547
|
+
title: row.title ? String(row.title) : null,
|
|
548
|
+
summary: row.summary ? String(row.summary) : null,
|
|
549
|
+
url: row.url ? String(row.url) : null,
|
|
550
|
+
additionalNotes: row.additional_notes ? String(row.additional_notes) : null,
|
|
551
|
+
providerPayload: parseJson(row.provider_payload_json, {}),
|
|
552
|
+
providerResponse: parseJson(row.provider_response_json, {}),
|
|
553
|
+
lastSyncedAt: row.last_synced_at ? String(row.last_synced_at) : null,
|
|
554
|
+
lastError: row.last_error ? String(row.last_error) : null,
|
|
555
|
+
cancelledAt: row.cancelled_at ? String(row.cancelled_at) : null,
|
|
556
|
+
sentAt: row.sent_at ? String(row.sent_at) : null,
|
|
557
|
+
createdAt: String(row.created_at),
|
|
558
|
+
updatedAt: String(row.updated_at)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function mapChatThread(row, tracers, messages = []) {
|
|
562
|
+
return {
|
|
563
|
+
id: String(row.id),
|
|
564
|
+
customerId: String(row.customer_id),
|
|
565
|
+
templateId: String(row.template_id),
|
|
566
|
+
title: String(row.title),
|
|
567
|
+
createdAt: String(row.created_at),
|
|
568
|
+
updatedAt: String(row.updated_at),
|
|
569
|
+
lastMessageAt: row.last_message_at ? String(row.last_message_at) : null,
|
|
570
|
+
messageCount: Number(row.message_count ?? 0),
|
|
571
|
+
tracers,
|
|
572
|
+
hiddenFromView: Boolean(row.hidden_from_view),
|
|
573
|
+
archivedAt: row.archived_at ? String(row.archived_at) : null,
|
|
574
|
+
storagePrefix: String(row.storage_prefix),
|
|
575
|
+
messages
|
|
576
|
+
};
|
|
577
|
+
}
|
|
332
578
|
export const database = {
|
|
333
579
|
raw: db,
|
|
334
580
|
upsertCustomer(input) {
|
|
@@ -337,9 +583,11 @@ export const database = {
|
|
|
337
583
|
db.prepare(`
|
|
338
584
|
insert into customers (
|
|
339
585
|
id, email, name, default_webhook_url, is_developer, is_paid_plan, about, groupchat_url, flockposter_api_key, password_hash, created_at, updated_at
|
|
586
|
+
, plan_tier
|
|
340
587
|
)
|
|
341
588
|
values (
|
|
342
589
|
@id, @email, @name, @default_webhook_url, @is_developer, @is_paid_plan, @about, @groupchat_url, @flockposter_api_key, @password_hash, @created_at, @updated_at
|
|
590
|
+
, @plan_tier
|
|
343
591
|
)
|
|
344
592
|
on conflict(id) do update set
|
|
345
593
|
email = excluded.email,
|
|
@@ -347,6 +595,7 @@ export const database = {
|
|
|
347
595
|
default_webhook_url = excluded.default_webhook_url,
|
|
348
596
|
is_developer = excluded.is_developer,
|
|
349
597
|
is_paid_plan = excluded.is_paid_plan,
|
|
598
|
+
plan_tier = excluded.plan_tier,
|
|
350
599
|
about = excluded.about,
|
|
351
600
|
groupchat_url = excluded.groupchat_url,
|
|
352
601
|
flockposter_api_key = excluded.flockposter_api_key,
|
|
@@ -359,6 +608,7 @@ export const database = {
|
|
|
359
608
|
default_webhook_url: input.defaultWebhookUrl ?? null,
|
|
360
609
|
is_developer: input.isDeveloper === undefined ? Number(current?.is_developer ?? 0) : (input.isDeveloper ? 1 : 0),
|
|
361
610
|
is_paid_plan: input.isPaidPlan === undefined ? Number(current?.is_paid_plan ?? 0) : (input.isPaidPlan ? 1 : 0),
|
|
611
|
+
plan_tier: input.planTier === undefined ? String(current?.plan_tier ?? "starter") : input.planTier,
|
|
362
612
|
about: input.about === undefined ? (current?.about ?? null) : input.about,
|
|
363
613
|
groupchat_url: input.groupchatUrl === undefined ? (current?.groupchat_url ?? null) : input.groupchatUrl,
|
|
364
614
|
flockposter_api_key: input.flockposterApiKey === undefined ? (current?.flockposter_api_key ?? null) : input.flockposterApiKey,
|
|
@@ -380,18 +630,42 @@ export const database = {
|
|
|
380
630
|
defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null,
|
|
381
631
|
isDeveloper: Boolean(row.is_developer),
|
|
382
632
|
isPaidPlan: Boolean(row.is_paid_plan),
|
|
633
|
+
planTier: normalizeCustomerPlanTier(row.plan_tier),
|
|
383
634
|
about: row.about ? String(row.about) : null,
|
|
384
635
|
groupchatUrl: row.groupchat_url ? String(row.groupchat_url) : null,
|
|
385
|
-
flockposterApiKey: row.flockposter_api_key ? String(row.flockposter_api_key) : null
|
|
636
|
+
flockposterApiKey: row.flockposter_api_key ? String(row.flockposter_api_key) : null,
|
|
637
|
+
createdAt: String(row.created_at),
|
|
638
|
+
updatedAt: String(row.updated_at)
|
|
386
639
|
};
|
|
387
640
|
},
|
|
388
641
|
getCustomerByEmail(email) {
|
|
389
642
|
const row = db.prepare(`select * from customers where lower(email) = lower(?)`).get(email);
|
|
390
643
|
return row ? this.getCustomerById(String(row.id)) : null;
|
|
391
644
|
},
|
|
645
|
+
getFirstPaidCustomer() {
|
|
646
|
+
const row = db.prepare(`
|
|
647
|
+
select id
|
|
648
|
+
from customers
|
|
649
|
+
where is_paid_plan = 1
|
|
650
|
+
order by created_at asc
|
|
651
|
+
limit 1
|
|
652
|
+
`).get();
|
|
653
|
+
return row?.id ? this.getCustomerById(String(row.id)) : null;
|
|
654
|
+
},
|
|
655
|
+
listPaidCustomers() {
|
|
656
|
+
const rows = db.prepare(`
|
|
657
|
+
select id
|
|
658
|
+
from customers
|
|
659
|
+
where is_paid_plan = 1
|
|
660
|
+
order by created_at asc
|
|
661
|
+
`).all();
|
|
662
|
+
return rows
|
|
663
|
+
.map((row) => this.getCustomerById(String(row.id)))
|
|
664
|
+
.filter((customer) => Boolean(customer));
|
|
665
|
+
},
|
|
392
666
|
getCustomerAuthByEmail(email) {
|
|
393
667
|
return db.prepare(`
|
|
394
|
-
select id, email, name, default_webhook_url, is_developer, is_paid_plan, password_hash
|
|
668
|
+
select id, email, name, default_webhook_url, is_developer, is_paid_plan, password_hash, plan_tier, created_at, updated_at
|
|
395
669
|
, about, groupchat_url, flockposter_api_key
|
|
396
670
|
from customers
|
|
397
671
|
where lower(email) = lower(?)
|
|
@@ -405,12 +679,36 @@ export const database = {
|
|
|
405
679
|
where id = ?
|
|
406
680
|
`).run(passwordHash, nowIso(), customerId);
|
|
407
681
|
},
|
|
408
|
-
updateCustomerPaidPlan(customerId, isPaidPlan) {
|
|
682
|
+
updateCustomerPaidPlan(customerId, isPaidPlan, planTier) {
|
|
683
|
+
const existing = this.getCustomerById(customerId);
|
|
409
684
|
db.prepare(`
|
|
410
685
|
update customers
|
|
411
|
-
set is_paid_plan = ?, updated_at = ?
|
|
686
|
+
set is_paid_plan = ?, plan_tier = ?, updated_at = ?
|
|
412
687
|
where id = ?
|
|
413
|
-
`).run(isPaidPlan ? 1 : 0, nowIso(), customerId);
|
|
688
|
+
`).run(isPaidPlan ? 1 : 0, planTier ?? existing?.planTier ?? "starter", nowIso(), customerId);
|
|
689
|
+
},
|
|
690
|
+
hasCustomerCreditGrant(customerId, grantType) {
|
|
691
|
+
const row = db.prepare(`
|
|
692
|
+
select 1
|
|
693
|
+
from customer_credit_grants
|
|
694
|
+
where customer_id = ? and grant_type = ?
|
|
695
|
+
limit 1
|
|
696
|
+
`).get(customerId, grantType);
|
|
697
|
+
return Boolean(row);
|
|
698
|
+
},
|
|
699
|
+
insertCustomerCreditGrant(record) {
|
|
700
|
+
const createdAt = nowIso();
|
|
701
|
+
db.prepare(`
|
|
702
|
+
insert into customer_credit_grants (id, customer_id, grant_type, amount_usd, metadata_json, created_at)
|
|
703
|
+
values (@id, @customer_id, @grant_type, @amount_usd, @metadata_json, @created_at)
|
|
704
|
+
`).run({
|
|
705
|
+
id: record.id,
|
|
706
|
+
customer_id: record.customerId,
|
|
707
|
+
grant_type: record.grantType,
|
|
708
|
+
amount_usd: record.amountUsd,
|
|
709
|
+
metadata_json: stringifyJson(record.metadata ?? {}),
|
|
710
|
+
created_at: createdAt
|
|
711
|
+
});
|
|
414
712
|
},
|
|
415
713
|
setCustomerAccessByEmail(input) {
|
|
416
714
|
const normalizedEmail = input.email.trim().toLowerCase();
|
|
@@ -422,21 +720,23 @@ export const database = {
|
|
|
422
720
|
defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
|
|
423
721
|
isDeveloper: input.isDeveloper === undefined ? (existing?.isDeveloper ?? false) : input.isDeveloper,
|
|
424
722
|
isPaidPlan: input.isPaidPlan === undefined ? (existing?.isPaidPlan ?? false) : input.isPaidPlan,
|
|
723
|
+
planTier: input.planTier === undefined ? (existing?.planTier ?? "starter") : input.planTier,
|
|
425
724
|
about: existing?.about ?? null,
|
|
426
725
|
groupchatUrl: existing?.groupchatUrl ?? null,
|
|
427
726
|
flockposterApiKey: existing?.flockposterApiKey ?? null
|
|
428
727
|
});
|
|
429
728
|
},
|
|
430
729
|
updateCustomerProfile(input) {
|
|
730
|
+
const existing = this.getCustomerById(input.customerId);
|
|
431
731
|
db.prepare(`
|
|
432
732
|
update customers
|
|
433
|
-
set about = ?, groupchat_url = ?, flockposter_api_key = ?, updated_at = ?
|
|
733
|
+
set name = ?, about = ?, groupchat_url = ?, flockposter_api_key = ?, updated_at = ?
|
|
434
734
|
where id = ?
|
|
435
|
-
`).run(input.about, input.groupchatUrl, input.flockposterApiKey, nowIso(), input.customerId);
|
|
735
|
+
`).run(input.name === undefined ? (existing?.name ?? null) : input.name, input.about === undefined ? (existing?.about ?? null) : input.about, input.groupchatUrl === undefined ? (existing?.groupchatUrl ?? null) : input.groupchatUrl, input.flockposterApiKey === undefined ? (existing?.flockposterApiKey ?? null) : input.flockposterApiKey, nowIso(), input.customerId);
|
|
436
736
|
},
|
|
437
737
|
listProviderKeys(customerId) {
|
|
438
738
|
return db.prepare(`
|
|
439
|
-
select id, provider, label, status,
|
|
739
|
+
select id, provider, label, status, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
|
|
440
740
|
from customer_provider_keys
|
|
441
741
|
where customer_id = ?
|
|
442
742
|
order by provider asc, created_at desc
|
|
@@ -444,7 +744,7 @@ export const database = {
|
|
|
444
744
|
},
|
|
445
745
|
listProviderKeysWithSecrets(customerId) {
|
|
446
746
|
return db.prepare(`
|
|
447
|
-
select id, provider, label, encrypted_secret as secret, status,
|
|
747
|
+
select id, provider, label, encrypted_secret as secret, status, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
|
|
448
748
|
from customer_provider_keys
|
|
449
749
|
where customer_id = ?
|
|
450
750
|
order by provider asc, created_at desc
|
|
@@ -453,6 +753,115 @@ export const database = {
|
|
|
453
753
|
deleteProviderKey(customerId, keyId) {
|
|
454
754
|
db.prepare(`delete from customer_provider_keys where id = ? and customer_id = ?`).run(keyId, customerId);
|
|
455
755
|
},
|
|
756
|
+
createEmailChannel(record) {
|
|
757
|
+
const timestamp = nowIso();
|
|
758
|
+
db.prepare(`
|
|
759
|
+
insert into email_channels (
|
|
760
|
+
id, customer_id, title, email, note, avatar_url, avatar_storage_key, status, verification_token_hash,
|
|
761
|
+
verification_sent_at, verified_at, created_at, updated_at
|
|
762
|
+
) values (
|
|
763
|
+
@id, @customer_id, @title, @email, @note, @avatar_url, @avatar_storage_key, @status, @verification_token_hash,
|
|
764
|
+
@verification_sent_at, null, @created_at, @updated_at
|
|
765
|
+
)
|
|
766
|
+
`).run({
|
|
767
|
+
id: record.id,
|
|
768
|
+
customer_id: record.customerId,
|
|
769
|
+
title: record.title,
|
|
770
|
+
email: record.email,
|
|
771
|
+
note: record.note ?? null,
|
|
772
|
+
avatar_url: record.avatarUrl ?? null,
|
|
773
|
+
avatar_storage_key: record.avatarStorageKey ?? null,
|
|
774
|
+
status: record.status,
|
|
775
|
+
verification_token_hash: record.verificationTokenHash ?? null,
|
|
776
|
+
verification_sent_at: record.verificationSentAt ?? null,
|
|
777
|
+
created_at: timestamp,
|
|
778
|
+
updated_at: timestamp
|
|
779
|
+
});
|
|
780
|
+
return this.getEmailChannel(record.customerId, record.id);
|
|
781
|
+
},
|
|
782
|
+
listEmailChannels(customerId) {
|
|
783
|
+
const rows = db.prepare(`
|
|
784
|
+
select *
|
|
785
|
+
from email_channels
|
|
786
|
+
where customer_id = ?
|
|
787
|
+
order by created_at desc
|
|
788
|
+
`).all(customerId);
|
|
789
|
+
return rows.map(mapEmailChannel);
|
|
790
|
+
},
|
|
791
|
+
getEmailChannel(customerId, channelId) {
|
|
792
|
+
const row = db.prepare(`
|
|
793
|
+
select *
|
|
794
|
+
from email_channels
|
|
795
|
+
where customer_id = ? and id = ?
|
|
796
|
+
limit 1
|
|
797
|
+
`).get(customerId, channelId);
|
|
798
|
+
return row ? mapEmailChannel(row) : null;
|
|
799
|
+
},
|
|
800
|
+
getEmailChannelByEmail(customerId, email) {
|
|
801
|
+
const row = db.prepare(`
|
|
802
|
+
select *
|
|
803
|
+
from email_channels
|
|
804
|
+
where customer_id = ? and lower(email) = lower(?)
|
|
805
|
+
limit 1
|
|
806
|
+
`).get(customerId, email);
|
|
807
|
+
return row ? mapEmailChannel(row) : null;
|
|
808
|
+
},
|
|
809
|
+
findEmailChannelByVerificationHash(tokenHash) {
|
|
810
|
+
const row = db.prepare(`
|
|
811
|
+
select *
|
|
812
|
+
from email_channels
|
|
813
|
+
where verification_token_hash = ?
|
|
814
|
+
limit 1
|
|
815
|
+
`).get(tokenHash);
|
|
816
|
+
return row ? mapEmailChannel(row) : null;
|
|
817
|
+
},
|
|
818
|
+
updateEmailChannel(input) {
|
|
819
|
+
db.prepare(`
|
|
820
|
+
update email_channels
|
|
821
|
+
set title = ?, note = ?, updated_at = ?
|
|
822
|
+
where customer_id = ? and id = ?
|
|
823
|
+
`).run(input.title, input.note ?? null, nowIso(), input.customerId, input.channelId);
|
|
824
|
+
return this.getEmailChannel(input.customerId, input.channelId);
|
|
825
|
+
},
|
|
826
|
+
updateEmailChannelAvatar(input) {
|
|
827
|
+
db.prepare(`
|
|
828
|
+
update email_channels
|
|
829
|
+
set avatar_url = ?, avatar_storage_key = ?, updated_at = ?
|
|
830
|
+
where customer_id = ? and id = ?
|
|
831
|
+
`).run(input.avatarUrl, input.avatarStorageKey, nowIso(), input.customerId, input.channelId);
|
|
832
|
+
return this.getEmailChannel(input.customerId, input.channelId);
|
|
833
|
+
},
|
|
834
|
+
updateEmailChannelVerification(input) {
|
|
835
|
+
db.prepare(`
|
|
836
|
+
update email_channels
|
|
837
|
+
set status = 'unverified',
|
|
838
|
+
verification_token_hash = ?,
|
|
839
|
+
verification_sent_at = ?,
|
|
840
|
+
verified_at = null,
|
|
841
|
+
updated_at = ?
|
|
842
|
+
where customer_id = ? and id = ?
|
|
843
|
+
`).run(input.tokenHash, input.sentAt, nowIso(), input.customerId, input.channelId);
|
|
844
|
+
return this.getEmailChannel(input.customerId, input.channelId);
|
|
845
|
+
},
|
|
846
|
+
verifyEmailChannelByTokenHash(tokenHash) {
|
|
847
|
+
const existing = this.findEmailChannelByVerificationHash(tokenHash);
|
|
848
|
+
if (!existing) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const timestamp = nowIso();
|
|
852
|
+
db.prepare(`
|
|
853
|
+
update email_channels
|
|
854
|
+
set status = 'verified',
|
|
855
|
+
verification_token_hash = null,
|
|
856
|
+
verified_at = ?,
|
|
857
|
+
updated_at = ?
|
|
858
|
+
where id = ?
|
|
859
|
+
`).run(timestamp, timestamp, existing.id);
|
|
860
|
+
return this.getEmailChannel(existing.customerId, existing.id);
|
|
861
|
+
},
|
|
862
|
+
deleteEmailChannel(customerId, channelId) {
|
|
863
|
+
db.prepare(`delete from email_channels where customer_id = ? and id = ?`).run(customerId, channelId);
|
|
864
|
+
},
|
|
456
865
|
insertOtpChallenge(record) {
|
|
457
866
|
db.prepare(`
|
|
458
867
|
insert into otp_challenges (id, email, code_hash, expires_at, created_at)
|
|
@@ -502,7 +911,7 @@ export const database = {
|
|
|
502
911
|
},
|
|
503
912
|
findApiKeyByHash(keyHash) {
|
|
504
913
|
return db.prepare(`
|
|
505
|
-
select a.*, c.email, c.name, c.default_webhook_url, c.is_developer, c.is_paid_plan, c.about, c.groupchat_url, c.flockposter_api_key
|
|
914
|
+
select a.*, c.email, c.name, c.default_webhook_url, c.is_developer, c.is_paid_plan, c.plan_tier, c.about, c.groupchat_url, c.flockposter_api_key, c.created_at, c.updated_at
|
|
506
915
|
from api_keys a
|
|
507
916
|
join customers c on c.id = a.customer_id
|
|
508
917
|
where a.key_hash = ? and a.status = 'active'
|
|
@@ -577,6 +986,17 @@ export const database = {
|
|
|
577
986
|
const row = db.prepare(`select * from jobs where id = ?`).get(id);
|
|
578
987
|
return row ? mapJob(row) : null;
|
|
579
988
|
},
|
|
989
|
+
getJobsByIds(ids) {
|
|
990
|
+
const uniqueIds = [...new Set(ids.map((id) => String(id).trim()).filter(Boolean))];
|
|
991
|
+
if (!uniqueIds.length) {
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
const rows = db.prepare(`
|
|
995
|
+
select * from jobs
|
|
996
|
+
where id in (${uniqueIds.map(() => "?").join(", ")})
|
|
997
|
+
`).all(...uniqueIds);
|
|
998
|
+
return rows.map(mapJob);
|
|
999
|
+
},
|
|
580
1000
|
countJobsByStatuses(input) {
|
|
581
1001
|
const statuses = input.statuses.length ? input.statuses : ["queued"];
|
|
582
1002
|
const filters = [`status in (${statuses.map(() => "?").join(", ")})`];
|
|
@@ -601,11 +1021,37 @@ export const database = {
|
|
|
601
1021
|
`).all(nowIso(), limit);
|
|
602
1022
|
return rows.map(mapJob);
|
|
603
1023
|
},
|
|
1024
|
+
listAvailableProviderKeyCounts() {
|
|
1025
|
+
const rows = db.prepare(`
|
|
1026
|
+
select
|
|
1027
|
+
k.customer_id,
|
|
1028
|
+
k.provider,
|
|
1029
|
+
count(*) as available_count
|
|
1030
|
+
from customer_provider_keys k
|
|
1031
|
+
left join provider_key_leases l
|
|
1032
|
+
on l.key_id = k.id
|
|
1033
|
+
and l.expires_at > ?
|
|
1034
|
+
where k.status = 'active'
|
|
1035
|
+
and (k.cooldown_until is null or k.cooldown_until <= ?)
|
|
1036
|
+
and l.key_id is null
|
|
1037
|
+
group by k.customer_id, k.provider
|
|
1038
|
+
`).all(nowIso(), nowIso());
|
|
1039
|
+
return rows.map((row) => ({
|
|
1040
|
+
customerId: row.customer_id,
|
|
1041
|
+
provider: row.provider,
|
|
1042
|
+
availableCount: Number(row.available_count),
|
|
1043
|
+
}));
|
|
1044
|
+
},
|
|
604
1045
|
markJobRunning(id) {
|
|
605
1046
|
const timestamp = nowIso();
|
|
606
1047
|
db.prepare(`
|
|
607
1048
|
update jobs
|
|
608
|
-
set status = 'running',
|
|
1049
|
+
set status = 'running',
|
|
1050
|
+
started_at = coalesce(started_at, ?),
|
|
1051
|
+
error_json = null,
|
|
1052
|
+
completed_at = null,
|
|
1053
|
+
updated_at = ?,
|
|
1054
|
+
attempt_count = attempt_count + 1
|
|
609
1055
|
where id = ?
|
|
610
1056
|
`).run(timestamp, timestamp, id);
|
|
611
1057
|
},
|
|
@@ -651,15 +1097,43 @@ export const database = {
|
|
|
651
1097
|
filters.push("created_at <= ?");
|
|
652
1098
|
values.push(input.endTime);
|
|
653
1099
|
}
|
|
654
|
-
|
|
1100
|
+
if (input.cursor) {
|
|
1101
|
+
filters.push("(created_at < ? or (created_at = ? and id < ?))");
|
|
1102
|
+
values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
|
|
1103
|
+
}
|
|
1104
|
+
values.push(Math.max(1, Math.min(input.limit ?? 100, 501)));
|
|
655
1105
|
const rows = db.prepare(`
|
|
656
1106
|
select * from jobs
|
|
657
1107
|
where ${filters.join(" and ")}
|
|
658
|
-
order by created_at desc
|
|
1108
|
+
order by created_at desc, id desc
|
|
659
1109
|
limit ?
|
|
660
1110
|
`).all(...values);
|
|
661
1111
|
return rows.map(mapJob);
|
|
662
1112
|
},
|
|
1113
|
+
renameJobsTracer(input) {
|
|
1114
|
+
const fromTracer = input.fromTracer.trim();
|
|
1115
|
+
const toTracer = input.toTracer.trim();
|
|
1116
|
+
if (!fromTracer || !toTracer || fromTracer === toTracer) {
|
|
1117
|
+
return 0;
|
|
1118
|
+
}
|
|
1119
|
+
const filters = ["customer_id = @customer_id", "tracer = @from_tracer"];
|
|
1120
|
+
if (input.templateId) {
|
|
1121
|
+
filters.push("template_id = @template_id");
|
|
1122
|
+
}
|
|
1123
|
+
const result = db.prepare(`
|
|
1124
|
+
update jobs
|
|
1125
|
+
set tracer = @to_tracer,
|
|
1126
|
+
updated_at = @updated_at
|
|
1127
|
+
where ${filters.join(" and ")}
|
|
1128
|
+
`).run({
|
|
1129
|
+
customer_id: input.customerId,
|
|
1130
|
+
template_id: input.templateId ?? null,
|
|
1131
|
+
from_tracer: fromTracer,
|
|
1132
|
+
to_tracer: toTracer,
|
|
1133
|
+
updated_at: nowIso()
|
|
1134
|
+
});
|
|
1135
|
+
return Number(result.changes || 0);
|
|
1136
|
+
},
|
|
663
1137
|
insertArtifact(record) {
|
|
664
1138
|
db.prepare(`
|
|
665
1139
|
insert into artifacts (id, job_id, customer_id, template_id, kind, storage_key, public_url, metadata_json, created_at)
|
|
@@ -676,21 +1150,26 @@ export const database = {
|
|
|
676
1150
|
created_at: nowIso()
|
|
677
1151
|
});
|
|
678
1152
|
},
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1153
|
+
listArtifactsForJob(input) {
|
|
1154
|
+
const filters = ["job_id = ?", "customer_id = ?"];
|
|
1155
|
+
const values = [input.jobId, input.customerId];
|
|
1156
|
+
if (input.templateId) {
|
|
1157
|
+
filters.push("template_id = ?");
|
|
1158
|
+
values.push(input.templateId);
|
|
1159
|
+
}
|
|
1160
|
+
if (input.cursor) {
|
|
1161
|
+
filters.push("(created_at < ? or (created_at = ? and id < ?))");
|
|
1162
|
+
values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
|
|
1163
|
+
}
|
|
1164
|
+
values.push(Math.max(1, Math.min(input.limit ?? 100, 501)));
|
|
1165
|
+
const rows = db.prepare(`
|
|
1166
|
+
select *
|
|
1167
|
+
from artifacts
|
|
1168
|
+
where ${filters.join(" and ")}
|
|
1169
|
+
order by created_at desc, id desc
|
|
1170
|
+
limit ?
|
|
1171
|
+
`).all(...values);
|
|
1172
|
+
return rows.map(mapArtifact);
|
|
694
1173
|
},
|
|
695
1174
|
queueWebhookDelivery(record) {
|
|
696
1175
|
const timestamp = nowIso();
|
|
@@ -743,6 +1222,28 @@ export const database = {
|
|
|
743
1222
|
acquireProviderKeyLease(input) {
|
|
744
1223
|
const transaction = db.transaction(() => {
|
|
745
1224
|
db.prepare(`delete from provider_key_leases where expires_at <= ?`).run(nowIso());
|
|
1225
|
+
db.prepare(`delete from provider_key_waiters where expires_at <= ?`).run(nowIso());
|
|
1226
|
+
db.prepare(`
|
|
1227
|
+
insert into provider_key_waiters (id, customer_id, provider, worker_id, owner_id, priority, created_at, expires_at)
|
|
1228
|
+
values (?, ?, ?, ?, ?, ?, coalesce((select created_at from provider_key_waiters where id = ?), ?), ?)
|
|
1229
|
+
on conflict(id) do update set
|
|
1230
|
+
worker_id = excluded.worker_id,
|
|
1231
|
+
owner_id = excluded.owner_id,
|
|
1232
|
+
priority = excluded.priority,
|
|
1233
|
+
expires_at = excluded.expires_at
|
|
1234
|
+
`).run(input.waiterId, input.customerId, input.provider, input.workerId, input.jobId, input.priority, input.waiterId, nowIso(), input.waiterExpiresAt);
|
|
1235
|
+
const nextWaiter = db.prepare(`
|
|
1236
|
+
select id
|
|
1237
|
+
from provider_key_waiters
|
|
1238
|
+
where customer_id = ?
|
|
1239
|
+
and provider = ?
|
|
1240
|
+
and expires_at > ?
|
|
1241
|
+
order by priority asc, created_at asc
|
|
1242
|
+
limit 1
|
|
1243
|
+
`).get(input.customerId, input.provider, nowIso());
|
|
1244
|
+
if (!nextWaiter || nextWaiter.id !== input.waiterId) {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
746
1247
|
const row = db.prepare(`
|
|
747
1248
|
select k.id, k.encrypted_secret
|
|
748
1249
|
from customer_provider_keys k
|
|
@@ -752,7 +1253,7 @@ export const database = {
|
|
|
752
1253
|
and k.status = 'active'
|
|
753
1254
|
and (k.cooldown_until is null or k.cooldown_until <= ?)
|
|
754
1255
|
and l.key_id is null
|
|
755
|
-
order by case when k.last_used_at is null then 0 else 1 end asc, k.last_used_at asc
|
|
1256
|
+
order by case when k.last_used_at is null then 0 else 1 end asc, k.last_used_at asc
|
|
756
1257
|
limit 1
|
|
757
1258
|
`).get(nowIso(), input.customerId, input.provider, nowIso());
|
|
758
1259
|
if (!row) {
|
|
@@ -762,10 +1263,14 @@ export const database = {
|
|
|
762
1263
|
insert into provider_key_leases (key_id, lease_token, worker_id, job_id, leased_at, expires_at)
|
|
763
1264
|
values (?, ?, ?, ?, ?, ?)
|
|
764
1265
|
`).run(row.id, input.leaseToken, input.workerId, input.jobId, nowIso(), input.expiresAt);
|
|
1266
|
+
db.prepare(`delete from provider_key_waiters where id = ?`).run(input.waiterId);
|
|
765
1267
|
return { keyId: row.id, encryptedSecret: row.encrypted_secret };
|
|
766
1268
|
});
|
|
767
1269
|
return transaction();
|
|
768
1270
|
},
|
|
1271
|
+
deleteProviderKeyWaiter(waiterId) {
|
|
1272
|
+
db.prepare(`delete from provider_key_waiters where id = ?`).run(waiterId);
|
|
1273
|
+
},
|
|
769
1274
|
releaseProviderKeyLease(input) {
|
|
770
1275
|
db.prepare(`delete from provider_key_leases where key_id = ? and lease_token = ?`).run(input.keyId, input.leaseToken);
|
|
771
1276
|
},
|
|
@@ -804,9 +1309,9 @@ export const database = {
|
|
|
804
1309
|
const timestamp = nowIso();
|
|
805
1310
|
db.prepare(`
|
|
806
1311
|
insert into customer_provider_keys (
|
|
807
|
-
id, customer_id, provider, label, encrypted_secret, status,
|
|
1312
|
+
id, customer_id, provider, label, encrypted_secret, status, created_at, updated_at
|
|
808
1313
|
) values (
|
|
809
|
-
@id, @customer_id, @provider, @label, @encrypted_secret, 'active', @
|
|
1314
|
+
@id, @customer_id, @provider, @label, @encrypted_secret, 'active', @created_at, @updated_at
|
|
810
1315
|
)
|
|
811
1316
|
`).run({
|
|
812
1317
|
id: record.id,
|
|
@@ -814,7 +1319,6 @@ export const database = {
|
|
|
814
1319
|
provider: record.provider,
|
|
815
1320
|
label: record.label,
|
|
816
1321
|
encrypted_secret: record.encryptedSecret,
|
|
817
|
-
weight: record.weight,
|
|
818
1322
|
created_at: timestamp,
|
|
819
1323
|
updated_at: timestamp
|
|
820
1324
|
});
|
|
@@ -858,6 +1362,281 @@ export const database = {
|
|
|
858
1362
|
deleteUserAttachment(customerId, attachmentId) {
|
|
859
1363
|
db.prepare(`delete from user_attachments where customer_id = ? and id = ?`).run(customerId, attachmentId);
|
|
860
1364
|
},
|
|
1365
|
+
createReadyPost(record) {
|
|
1366
|
+
const timestamp = nowIso();
|
|
1367
|
+
db.prepare(`
|
|
1368
|
+
insert into ready_posts (
|
|
1369
|
+
id, customer_id, status, tracer, title, content, pinned_comment, scheduled_at, timezone, post_type,
|
|
1370
|
+
integration_ids_json, platform_targets_json, media_assets_json, renditions_json,
|
|
1371
|
+
flockposter_payload_json, field_values_json, share_password_hash, zip_storage_key,
|
|
1372
|
+
zip_public_url, archived_at, deleted_at, created_at, updated_at
|
|
1373
|
+
) values (
|
|
1374
|
+
@id, @customer_id, @status, @tracer, @title, @content, @pinned_comment, @scheduled_at, @timezone, @post_type,
|
|
1375
|
+
@integration_ids_json, @platform_targets_json, @media_assets_json, @renditions_json,
|
|
1376
|
+
@flockposter_payload_json, @field_values_json, @share_password_hash, @zip_storage_key,
|
|
1377
|
+
@zip_public_url, @archived_at, @deleted_at, @created_at, @updated_at
|
|
1378
|
+
)
|
|
1379
|
+
`).run({
|
|
1380
|
+
id: record.id,
|
|
1381
|
+
customer_id: record.customerId,
|
|
1382
|
+
status: record.status,
|
|
1383
|
+
tracer: record.tracer,
|
|
1384
|
+
title: record.title,
|
|
1385
|
+
content: record.caption,
|
|
1386
|
+
pinned_comment: record.pinnedComment,
|
|
1387
|
+
scheduled_at: null,
|
|
1388
|
+
timezone: null,
|
|
1389
|
+
post_type: "draft",
|
|
1390
|
+
integration_ids_json: stringifyJson([]),
|
|
1391
|
+
platform_targets_json: stringifyJson([]),
|
|
1392
|
+
media_assets_json: stringifyJson(record.mediaAssets),
|
|
1393
|
+
renditions_json: stringifyJson([]),
|
|
1394
|
+
flockposter_payload_json: stringifyJson({}),
|
|
1395
|
+
field_values_json: stringifyJson({}),
|
|
1396
|
+
share_password_hash: record.sharePasswordHash,
|
|
1397
|
+
zip_storage_key: record.zipStorageKey ?? null,
|
|
1398
|
+
zip_public_url: record.zipPublicUrl ?? null,
|
|
1399
|
+
archived_at: record.archivedAt ?? null,
|
|
1400
|
+
deleted_at: record.deletedAt ?? null,
|
|
1401
|
+
created_at: timestamp,
|
|
1402
|
+
updated_at: timestamp
|
|
1403
|
+
});
|
|
1404
|
+
return this.getReadyPost(record.id);
|
|
1405
|
+
},
|
|
1406
|
+
getReadyPost(id) {
|
|
1407
|
+
const row = db.prepare(`select * from ready_posts where id = ? limit 1`).get(id);
|
|
1408
|
+
return row ? mapReadyPost(row) : null;
|
|
1409
|
+
},
|
|
1410
|
+
listReadyPosts(customerId, input) {
|
|
1411
|
+
const filters = ["customer_id = ?", "status != 'deleted'"];
|
|
1412
|
+
const values = [customerId];
|
|
1413
|
+
if (input?.cursor) {
|
|
1414
|
+
filters.push("(created_at < ? or (created_at = ? and id < ?))");
|
|
1415
|
+
values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
|
|
1416
|
+
}
|
|
1417
|
+
const rows = db.prepare(`
|
|
1418
|
+
select *
|
|
1419
|
+
from ready_posts
|
|
1420
|
+
where ${filters.join(" and ")}
|
|
1421
|
+
order by created_at desc, id desc
|
|
1422
|
+
${input?.limit || input?.cursor ? "limit ?" : ""}
|
|
1423
|
+
`).all(...(input?.limit || input?.cursor ? [...values, Math.max(1, Math.min(input?.limit ?? 100, 501))] : values));
|
|
1424
|
+
return rows.map(mapReadyPost);
|
|
1425
|
+
},
|
|
1426
|
+
archiveReadyPost(customerId, postId) {
|
|
1427
|
+
const timestamp = nowIso();
|
|
1428
|
+
db.prepare(`
|
|
1429
|
+
update ready_posts
|
|
1430
|
+
set status = 'archived', archived_at = coalesce(archived_at, ?), updated_at = ?
|
|
1431
|
+
where id = ? and customer_id = ? and status != 'deleted'
|
|
1432
|
+
`).run(timestamp, timestamp, postId, customerId);
|
|
1433
|
+
return this.getReadyPost(postId);
|
|
1434
|
+
},
|
|
1435
|
+
deleteReadyPost(customerId, postId) {
|
|
1436
|
+
const timestamp = nowIso();
|
|
1437
|
+
db.prepare(`
|
|
1438
|
+
update ready_posts
|
|
1439
|
+
set status = 'deleted', deleted_at = coalesce(deleted_at, ?), updated_at = ?
|
|
1440
|
+
where id = ? and customer_id = ?
|
|
1441
|
+
`).run(timestamp, timestamp, postId, customerId);
|
|
1442
|
+
return this.getReadyPost(postId);
|
|
1443
|
+
},
|
|
1444
|
+
setReadyPostZip(input) {
|
|
1445
|
+
db.prepare(`
|
|
1446
|
+
update ready_posts
|
|
1447
|
+
set zip_storage_key = ?, zip_public_url = ?, updated_at = ?
|
|
1448
|
+
where id = ? and customer_id = ? and status != 'deleted'
|
|
1449
|
+
`).run(input.storageKey, input.publicUrl, nowIso(), input.postId, input.customerId);
|
|
1450
|
+
return this.getReadyPost(input.postId);
|
|
1451
|
+
},
|
|
1452
|
+
createPostSchedule(record) {
|
|
1453
|
+
const timestamp = nowIso();
|
|
1454
|
+
db.prepare(`
|
|
1455
|
+
insert into post_schedules (
|
|
1456
|
+
id, customer_id, ready_post_id, managed_by, provider, destination_type, destination_id,
|
|
1457
|
+
scheduled_at, timezone, status, provider_status, provider_schedule_id, title, summary, url,
|
|
1458
|
+
additional_notes, provider_payload_json, provider_response_json, last_synced_at, last_error, cancelled_at, sent_at,
|
|
1459
|
+
created_at, updated_at
|
|
1460
|
+
) values (
|
|
1461
|
+
@id, @customer_id, @ready_post_id, @managed_by, @provider, @destination_type, @destination_id,
|
|
1462
|
+
@scheduled_at, @timezone, @status, @provider_status, @provider_schedule_id, @title, @summary, @url,
|
|
1463
|
+
@additional_notes, @provider_payload_json, @provider_response_json, @last_synced_at, @last_error, @cancelled_at, @sent_at,
|
|
1464
|
+
@created_at, @updated_at
|
|
1465
|
+
)
|
|
1466
|
+
`).run({
|
|
1467
|
+
id: record.id,
|
|
1468
|
+
customer_id: record.customerId,
|
|
1469
|
+
ready_post_id: record.readyPostId,
|
|
1470
|
+
managed_by: record.managedBy,
|
|
1471
|
+
provider: record.provider,
|
|
1472
|
+
destination_type: record.destinationType,
|
|
1473
|
+
destination_id: record.destinationId,
|
|
1474
|
+
scheduled_at: record.scheduledAt,
|
|
1475
|
+
timezone: record.timezone,
|
|
1476
|
+
status: record.status,
|
|
1477
|
+
provider_status: record.providerStatus,
|
|
1478
|
+
provider_schedule_id: record.providerScheduleId,
|
|
1479
|
+
title: record.title,
|
|
1480
|
+
summary: record.summary,
|
|
1481
|
+
url: record.url,
|
|
1482
|
+
additional_notes: record.additionalNotes,
|
|
1483
|
+
provider_payload_json: stringifyJson(record.providerPayload),
|
|
1484
|
+
provider_response_json: stringifyJson(record.providerResponse),
|
|
1485
|
+
last_synced_at: record.lastSyncedAt,
|
|
1486
|
+
last_error: record.lastError,
|
|
1487
|
+
cancelled_at: record.cancelledAt,
|
|
1488
|
+
sent_at: record.sentAt,
|
|
1489
|
+
created_at: timestamp,
|
|
1490
|
+
updated_at: timestamp
|
|
1491
|
+
});
|
|
1492
|
+
if (record.readyPostId) {
|
|
1493
|
+
this.refreshReadyPostScheduleStatus(record.customerId, record.readyPostId);
|
|
1494
|
+
}
|
|
1495
|
+
return this.getPostSchedule(record.customerId, record.id);
|
|
1496
|
+
},
|
|
1497
|
+
getPostSchedule(customerId, scheduleId) {
|
|
1498
|
+
const row = db.prepare(`
|
|
1499
|
+
select *
|
|
1500
|
+
from post_schedules
|
|
1501
|
+
where customer_id = ? and id = ?
|
|
1502
|
+
limit 1
|
|
1503
|
+
`).get(customerId, scheduleId);
|
|
1504
|
+
return row ? mapPostSchedule(row) : null;
|
|
1505
|
+
},
|
|
1506
|
+
listPostSchedulesForPost(customerId, postId) {
|
|
1507
|
+
const rows = db.prepare(`
|
|
1508
|
+
select *
|
|
1509
|
+
from post_schedules
|
|
1510
|
+
where customer_id = ? and ready_post_id = ?
|
|
1511
|
+
order by scheduled_at asc, created_at asc
|
|
1512
|
+
`).all(customerId, postId);
|
|
1513
|
+
return rows.map(mapPostSchedule);
|
|
1514
|
+
},
|
|
1515
|
+
listPostSchedulesForCalendar(input) {
|
|
1516
|
+
const rows = db.prepare(`
|
|
1517
|
+
select *
|
|
1518
|
+
from post_schedules
|
|
1519
|
+
where customer_id = ?
|
|
1520
|
+
and scheduled_at >= ?
|
|
1521
|
+
and scheduled_at <= ?
|
|
1522
|
+
and status != 'cancelled'
|
|
1523
|
+
order by scheduled_at asc, created_at asc
|
|
1524
|
+
`).all(input.customerId, input.startDate, input.endDate);
|
|
1525
|
+
return rows.map(mapPostSchedule);
|
|
1526
|
+
},
|
|
1527
|
+
updatePostScheduleProvider(input) {
|
|
1528
|
+
db.prepare(`
|
|
1529
|
+
update post_schedules
|
|
1530
|
+
set scheduled_at = ?,
|
|
1531
|
+
timezone = ?,
|
|
1532
|
+
status = ?,
|
|
1533
|
+
provider_status = ?,
|
|
1534
|
+
provider_schedule_id = ?,
|
|
1535
|
+
provider_payload_json = ?,
|
|
1536
|
+
provider_response_json = ?,
|
|
1537
|
+
last_synced_at = ?,
|
|
1538
|
+
last_error = ?,
|
|
1539
|
+
url = coalesce(?, url),
|
|
1540
|
+
additional_notes = ?,
|
|
1541
|
+
cancelled_at = null,
|
|
1542
|
+
updated_at = ?
|
|
1543
|
+
where customer_id = ? and id = ?
|
|
1544
|
+
`).run(input.scheduledAt, input.timezone ?? null, input.status ?? "scheduled", input.providerStatus ?? null, input.providerScheduleId ?? null, stringifyJson(input.providerPayload), stringifyJson(input.providerResponse), nowIso(), input.lastError ?? null, input.url ?? null, input.additionalNotes ?? null, nowIso(), input.customerId, input.scheduleId);
|
|
1545
|
+
const updated = this.getPostSchedule(input.customerId, input.scheduleId);
|
|
1546
|
+
if (updated?.readyPostId) {
|
|
1547
|
+
this.refreshReadyPostScheduleStatus(input.customerId, updated.readyPostId);
|
|
1548
|
+
}
|
|
1549
|
+
return updated;
|
|
1550
|
+
},
|
|
1551
|
+
markPostScheduleCancelled(input) {
|
|
1552
|
+
const timestamp = nowIso();
|
|
1553
|
+
const existing = this.getPostSchedule(input.customerId, input.scheduleId);
|
|
1554
|
+
db.prepare(`
|
|
1555
|
+
update post_schedules
|
|
1556
|
+
set status = 'cancelled',
|
|
1557
|
+
provider_response_json = ?,
|
|
1558
|
+
last_synced_at = ?,
|
|
1559
|
+
last_error = ?,
|
|
1560
|
+
cancelled_at = coalesce(cancelled_at, ?),
|
|
1561
|
+
updated_at = ?
|
|
1562
|
+
where customer_id = ? and id = ?
|
|
1563
|
+
`).run(stringifyJson(input.providerResponse ?? existing?.providerResponse ?? {}), timestamp, input.lastError ?? null, timestamp, timestamp, input.customerId, input.scheduleId);
|
|
1564
|
+
if (existing?.readyPostId) {
|
|
1565
|
+
this.refreshReadyPostScheduleStatus(input.customerId, existing.readyPostId);
|
|
1566
|
+
}
|
|
1567
|
+
return this.getPostSchedule(input.customerId, input.scheduleId);
|
|
1568
|
+
},
|
|
1569
|
+
markPostScheduleFailed(input) {
|
|
1570
|
+
const existing = this.getPostSchedule(input.customerId, input.scheduleId);
|
|
1571
|
+
db.prepare(`
|
|
1572
|
+
update post_schedules
|
|
1573
|
+
set status = 'failed',
|
|
1574
|
+
provider_response_json = ?,
|
|
1575
|
+
last_synced_at = ?,
|
|
1576
|
+
last_error = ?,
|
|
1577
|
+
updated_at = ?
|
|
1578
|
+
where customer_id = ? and id = ?
|
|
1579
|
+
`).run(stringifyJson(input.providerResponse ?? existing?.providerResponse ?? {}), nowIso(), input.lastError, nowIso(), input.customerId, input.scheduleId);
|
|
1580
|
+
if (existing?.readyPostId) {
|
|
1581
|
+
this.refreshReadyPostScheduleStatus(input.customerId, existing.readyPostId);
|
|
1582
|
+
}
|
|
1583
|
+
return this.getPostSchedule(input.customerId, input.scheduleId);
|
|
1584
|
+
},
|
|
1585
|
+
upsertExternalPostSchedule(record) {
|
|
1586
|
+
const timestamp = nowIso();
|
|
1587
|
+
const existing = record.providerScheduleId
|
|
1588
|
+
? db.prepare(`
|
|
1589
|
+
select *
|
|
1590
|
+
from post_schedules
|
|
1591
|
+
where customer_id = ? and provider = ? and provider_schedule_id = ?
|
|
1592
|
+
limit 1
|
|
1593
|
+
`).get(record.customerId, record.provider, record.providerScheduleId)
|
|
1594
|
+
: undefined;
|
|
1595
|
+
if (existing) {
|
|
1596
|
+
db.prepare(`
|
|
1597
|
+
update post_schedules
|
|
1598
|
+
set destination_id = ?,
|
|
1599
|
+
scheduled_at = ?,
|
|
1600
|
+
status = ?,
|
|
1601
|
+
provider_status = ?,
|
|
1602
|
+
title = ?,
|
|
1603
|
+
summary = ?,
|
|
1604
|
+
url = ?,
|
|
1605
|
+
additional_notes = ?,
|
|
1606
|
+
provider_payload_json = ?,
|
|
1607
|
+
provider_response_json = ?,
|
|
1608
|
+
last_synced_at = ?,
|
|
1609
|
+
last_error = null,
|
|
1610
|
+
updated_at = ?
|
|
1611
|
+
where id = ?
|
|
1612
|
+
`).run(record.destinationId, record.scheduledAt, record.status, record.providerStatus, record.title, record.summary, record.url, record.additionalNotes ?? null, stringifyJson(record.providerPayload), stringifyJson(record.providerResponse), timestamp, timestamp, String(existing.id));
|
|
1613
|
+
return this.getPostSchedule(record.customerId, String(existing.id));
|
|
1614
|
+
}
|
|
1615
|
+
return this.createPostSchedule({
|
|
1616
|
+
...record,
|
|
1617
|
+
readyPostId: null,
|
|
1618
|
+
managedBy: "external",
|
|
1619
|
+
timezone: null,
|
|
1620
|
+
additionalNotes: record.additionalNotes ?? null,
|
|
1621
|
+
cancelledAt: null,
|
|
1622
|
+
sentAt: null,
|
|
1623
|
+
lastError: null
|
|
1624
|
+
});
|
|
1625
|
+
},
|
|
1626
|
+
refreshReadyPostScheduleStatus(customerId, postId) {
|
|
1627
|
+
const row = db.prepare(`
|
|
1628
|
+
select count(*) as count
|
|
1629
|
+
from post_schedules
|
|
1630
|
+
where customer_id = ? and ready_post_id = ? and status = 'scheduled'
|
|
1631
|
+
`).get(customerId, postId);
|
|
1632
|
+
const nextStatus = row && Number(row.count) > 0 ? "scheduled" : "ready";
|
|
1633
|
+
db.prepare(`
|
|
1634
|
+
update ready_posts
|
|
1635
|
+
set status = ?, updated_at = ?
|
|
1636
|
+
where customer_id = ? and id = ? and status in ('ready', 'scheduled')
|
|
1637
|
+
`).run(nextStatus, nowIso(), customerId, postId);
|
|
1638
|
+
return this.getReadyPost(postId);
|
|
1639
|
+
},
|
|
861
1640
|
createTemplateSource(record) {
|
|
862
1641
|
const timestamp = nowIso();
|
|
863
1642
|
db.prepare(`
|
|
@@ -932,10 +1711,24 @@ export const database = {
|
|
|
932
1711
|
`).get(repoUrl, branch, templateModulePath);
|
|
933
1712
|
return row ? mapTemplateSource(row) : null;
|
|
934
1713
|
},
|
|
935
|
-
listTemplateSources(
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
1714
|
+
listTemplateSources(input) {
|
|
1715
|
+
const filters = [];
|
|
1716
|
+
const values = [];
|
|
1717
|
+
if (input?.status) {
|
|
1718
|
+
filters.push("status = ?");
|
|
1719
|
+
values.push(input.status);
|
|
1720
|
+
}
|
|
1721
|
+
if (input?.cursor) {
|
|
1722
|
+
filters.push("(created_at < ? or (created_at = ? and id < ?))");
|
|
1723
|
+
values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
|
|
1724
|
+
}
|
|
1725
|
+
const rows = db.prepare(`
|
|
1726
|
+
select *
|
|
1727
|
+
from template_sources
|
|
1728
|
+
${filters.length ? `where ${filters.join(" and ")}` : ""}
|
|
1729
|
+
order by created_at desc, id desc
|
|
1730
|
+
${input?.limit || input?.cursor ? "limit ?" : ""}
|
|
1731
|
+
`).all(...(input?.limit || input?.cursor ? [...values, Math.max(1, Math.min(input?.limit ?? 100, 501))] : values));
|
|
939
1732
|
return rows.map(mapTemplateSource);
|
|
940
1733
|
},
|
|
941
1734
|
createTemplateRelease(record) {
|
|
@@ -996,15 +1789,27 @@ export const database = {
|
|
|
996
1789
|
return row ? mapTemplateRelease(row) : null;
|
|
997
1790
|
},
|
|
998
1791
|
listTemplateReleases(input) {
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1792
|
+
const filters = [];
|
|
1793
|
+
const values = [];
|
|
1794
|
+
if (input?.templateId) {
|
|
1795
|
+
filters.push("template_id = ?");
|
|
1796
|
+
values.push(input.templateId);
|
|
1797
|
+
}
|
|
1798
|
+
if (input?.status) {
|
|
1799
|
+
filters.push("status = ?");
|
|
1800
|
+
values.push(input.status);
|
|
1801
|
+
}
|
|
1802
|
+
if (input?.cursor) {
|
|
1803
|
+
filters.push("(created_at < ? or (created_at = ? and id < ?))");
|
|
1804
|
+
values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
|
|
1805
|
+
}
|
|
1806
|
+
const rows = db.prepare(`
|
|
1807
|
+
select *
|
|
1808
|
+
from template_releases
|
|
1809
|
+
${filters.length ? `where ${filters.join(" and ")}` : ""}
|
|
1810
|
+
order by created_at desc, id desc
|
|
1811
|
+
${input?.limit || input?.cursor ? "limit ?" : ""}
|
|
1812
|
+
`).all(...(input?.limit || input?.cursor ? [...values, Math.max(1, Math.min(input?.limit ?? 100, 500))] : values));
|
|
1008
1813
|
return rows.map(mapTemplateRelease);
|
|
1009
1814
|
},
|
|
1010
1815
|
getActiveTemplateReleases() {
|
|
@@ -1040,5 +1845,163 @@ export const database = {
|
|
|
1040
1845
|
updated_at: nowIso(),
|
|
1041
1846
|
template_id: templateId
|
|
1042
1847
|
});
|
|
1848
|
+
},
|
|
1849
|
+
upsertChatThread(input) {
|
|
1850
|
+
const timestamp = nowIso();
|
|
1851
|
+
const current = this.getChatThreadMeta(input.customerId, input.id);
|
|
1852
|
+
db.prepare(`
|
|
1853
|
+
insert into chat_threads (
|
|
1854
|
+
id, customer_id, template_id, title, storage_prefix, message_count, last_message_at,
|
|
1855
|
+
hidden_from_view, archived_at, created_at, updated_at
|
|
1856
|
+
) values (
|
|
1857
|
+
@id, @customer_id, @template_id, @title, @storage_prefix, @message_count, @last_message_at,
|
|
1858
|
+
@hidden_from_view, @archived_at, @created_at, @updated_at
|
|
1859
|
+
)
|
|
1860
|
+
on conflict(id) do update set
|
|
1861
|
+
template_id = excluded.template_id,
|
|
1862
|
+
title = excluded.title,
|
|
1863
|
+
storage_prefix = excluded.storage_prefix,
|
|
1864
|
+
message_count = excluded.message_count,
|
|
1865
|
+
last_message_at = excluded.last_message_at,
|
|
1866
|
+
hidden_from_view = excluded.hidden_from_view,
|
|
1867
|
+
archived_at = excluded.archived_at,
|
|
1868
|
+
updated_at = excluded.updated_at
|
|
1869
|
+
`).run({
|
|
1870
|
+
id: input.id,
|
|
1871
|
+
customer_id: input.customerId,
|
|
1872
|
+
template_id: input.templateId,
|
|
1873
|
+
title: input.title,
|
|
1874
|
+
storage_prefix: input.storagePrefix,
|
|
1875
|
+
message_count: input.messageCount ?? current?.messageCount ?? 0,
|
|
1876
|
+
last_message_at: input.lastMessageAt ?? current?.lastMessageAt ?? null,
|
|
1877
|
+
hidden_from_view: input.hiddenFromView === undefined ? (current?.hiddenFromView ? 1 : 0) : (input.hiddenFromView ? 1 : 0),
|
|
1878
|
+
archived_at: input.archivedAt === undefined ? (current?.archivedAt ?? null) : input.archivedAt,
|
|
1879
|
+
created_at: current?.createdAt ?? timestamp,
|
|
1880
|
+
updated_at: timestamp
|
|
1881
|
+
});
|
|
1882
|
+
if (input.tracers) {
|
|
1883
|
+
this.replaceChatThreadTracers(input.id, input.tracers);
|
|
1884
|
+
}
|
|
1885
|
+
return this.getChatThreadMeta(input.customerId, input.id);
|
|
1886
|
+
},
|
|
1887
|
+
appendChatThreadMessages(input) {
|
|
1888
|
+
const current = this.getChatThreadMeta(input.customerId, input.threadId);
|
|
1889
|
+
this.upsertChatThread({
|
|
1890
|
+
id: input.threadId,
|
|
1891
|
+
customerId: input.customerId,
|
|
1892
|
+
templateId: input.templateId,
|
|
1893
|
+
title: input.title || current?.title || "New chat",
|
|
1894
|
+
storagePrefix: input.storagePrefix,
|
|
1895
|
+
tracers: input.tracers,
|
|
1896
|
+
hiddenFromView: current?.hiddenFromView ?? false,
|
|
1897
|
+
archivedAt: current?.archivedAt ?? null,
|
|
1898
|
+
messageCount: (current?.messageCount ?? 0) + Math.max(0, input.messageCountDelta),
|
|
1899
|
+
lastMessageAt: input.lastMessageAt ?? current?.lastMessageAt ?? null
|
|
1900
|
+
});
|
|
1901
|
+
return this.getChatThreadMeta(input.customerId, input.threadId);
|
|
1902
|
+
},
|
|
1903
|
+
getChatThreadMeta(customerId, threadId) {
|
|
1904
|
+
const row = db.prepare(`
|
|
1905
|
+
select *
|
|
1906
|
+
from chat_threads
|
|
1907
|
+
where customer_id = ? and id = ?
|
|
1908
|
+
limit 1
|
|
1909
|
+
`).get(customerId, threadId);
|
|
1910
|
+
if (!row) {
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
const tracers = db.prepare(`
|
|
1914
|
+
select tracer
|
|
1915
|
+
from chat_thread_tracers
|
|
1916
|
+
where thread_id = ?
|
|
1917
|
+
order by created_at asc, tracer asc
|
|
1918
|
+
`).all(threadId);
|
|
1919
|
+
return mapChatThread(row, tracers.map((entry) => String(entry.tracer)));
|
|
1920
|
+
},
|
|
1921
|
+
listChatThreadMetas(input) {
|
|
1922
|
+
const filters = ["customer_id = ?"];
|
|
1923
|
+
const values = [input.customerId];
|
|
1924
|
+
if (input.templateId) {
|
|
1925
|
+
filters.push("template_id = ?");
|
|
1926
|
+
values.push(input.templateId);
|
|
1927
|
+
}
|
|
1928
|
+
if (input.cursor) {
|
|
1929
|
+
filters.push("(updated_at < ? or (updated_at = ? and id < ?))");
|
|
1930
|
+
values.push(input.cursor.updatedAt, input.cursor.updatedAt, input.cursor.id);
|
|
1931
|
+
}
|
|
1932
|
+
values.push(Math.max(1, Math.min(input.limit ?? 100, 501)));
|
|
1933
|
+
const rows = db.prepare(`
|
|
1934
|
+
select *
|
|
1935
|
+
from chat_threads
|
|
1936
|
+
where ${filters.join(" and ")}
|
|
1937
|
+
order by updated_at desc, id desc
|
|
1938
|
+
limit ?
|
|
1939
|
+
`).all(...values);
|
|
1940
|
+
if (!rows.length) {
|
|
1941
|
+
return [];
|
|
1942
|
+
}
|
|
1943
|
+
const tracerRows = db.prepare(`
|
|
1944
|
+
select thread_id, tracer
|
|
1945
|
+
from chat_thread_tracers
|
|
1946
|
+
where thread_id in (${rows.map(() => "?").join(", ")})
|
|
1947
|
+
order by created_at asc, tracer asc
|
|
1948
|
+
`).all(...rows.map((row) => String(row.id)));
|
|
1949
|
+
const tracerMap = new Map();
|
|
1950
|
+
for (const row of tracerRows) {
|
|
1951
|
+
const list = tracerMap.get(String(row.thread_id)) ?? [];
|
|
1952
|
+
list.push(String(row.tracer));
|
|
1953
|
+
tracerMap.set(String(row.thread_id), list);
|
|
1954
|
+
}
|
|
1955
|
+
return rows.map((row) => mapChatThread(row, tracerMap.get(String(row.id)) ?? []));
|
|
1956
|
+
},
|
|
1957
|
+
replaceChatThreadTracers(threadId, tracers) {
|
|
1958
|
+
const normalized = [...new Set(tracers.map((value) => value.trim()).filter(Boolean))];
|
|
1959
|
+
const timestamp = nowIso();
|
|
1960
|
+
const transaction = db.transaction(() => {
|
|
1961
|
+
db.prepare(`delete from chat_thread_tracers where thread_id = ?`).run(threadId);
|
|
1962
|
+
const insert = db.prepare(`
|
|
1963
|
+
insert into chat_thread_tracers (thread_id, tracer, created_at)
|
|
1964
|
+
values (?, ?, ?)
|
|
1965
|
+
`);
|
|
1966
|
+
for (const tracer of normalized) {
|
|
1967
|
+
insert.run(threadId, tracer, timestamp);
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
transaction();
|
|
1971
|
+
},
|
|
1972
|
+
updateChatThreadState(input) {
|
|
1973
|
+
const current = this.getChatThreadMeta(input.customerId, input.threadId);
|
|
1974
|
+
if (!current) {
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
db.prepare(`
|
|
1978
|
+
update chat_threads
|
|
1979
|
+
set title = ?,
|
|
1980
|
+
hidden_from_view = ?,
|
|
1981
|
+
archived_at = ?,
|
|
1982
|
+
updated_at = ?
|
|
1983
|
+
where customer_id = ? and id = ?
|
|
1984
|
+
`).run(input.title ?? current.title, input.hiddenFromView === undefined ? (current.hiddenFromView ? 1 : 0) : (input.hiddenFromView ? 1 : 0), input.archivedAt === undefined ? current.archivedAt : input.archivedAt, nowIso(), input.customerId, input.threadId);
|
|
1985
|
+
if (input.tracers) {
|
|
1986
|
+
this.replaceChatThreadTracers(input.threadId, input.tracers);
|
|
1987
|
+
}
|
|
1988
|
+
return this.getChatThreadMeta(input.customerId, input.threadId);
|
|
1989
|
+
},
|
|
1990
|
+
deleteChatThread(customerId, threadId) {
|
|
1991
|
+
const current = this.getChatThreadMeta(customerId, threadId);
|
|
1992
|
+
if (!current) {
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
db.prepare(`
|
|
1996
|
+
delete from chat_threads
|
|
1997
|
+
where customer_id = ? and id = ?
|
|
1998
|
+
`).run(customerId, threadId);
|
|
1999
|
+
return current;
|
|
1043
2000
|
}
|
|
1044
2001
|
};
|
|
2002
|
+
function normalizeCustomerPlanTier(value) {
|
|
2003
|
+
if (value === "starter" || value === "standard" || value === "agency") {
|
|
2004
|
+
return value;
|
|
2005
|
+
}
|
|
2006
|
+
return "starter";
|
|
2007
|
+
}
|