@mevdragon/vidfarm-devcli 0.2.7 → 0.2.9
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 -30
- package/README.md +22 -5
- package/SKILL.developer.md +464 -12
- 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 +7173 -494
- package/dist/src/cli.js +525 -29
- package/dist/src/config.js +14 -7
- package/dist/src/context.js +51 -35
- package/dist/src/db.js +1049 -55
- 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 -339
- package/dist/src/lib/template-paths.js +10 -4
- 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 +44 -18
- package/dist/src/runtime.js +6 -1
- package/dist/src/services/api-call-history.js +245 -0
- package/dist/src/services/auth.js +27 -9
- package/dist/src/services/billing.js +248 -9
- package/dist/src/services/chat-threads.js +88 -0
- package/dist/src/services/job-logs.js +10 -3
- package/dist/src/services/jobs.js +13 -2
- package/dist/src/services/providers.js +944 -55
- package/dist/src/services/rate-limits.js +236 -0
- package/dist/src/services/remotion.js +143 -16
- package/dist/src/services/storage.js +23 -8
- package/dist/src/services/template-certification.js +26 -3
- package/dist/src/services/template-loader.js +19 -1
- package/dist/src/services/template-sources.js +316 -7
- 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 +30 -6
- 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/template_0000/src/lib/images.js +0 -202
- package/dist/templates/template_0000/src/remotion/Root.js +0 -33
- package/dist/templates/template_0000/src/remotion/index.js +0 -3
- package/dist/templates/template_0000/src/sdk.js +0 -3
- package/dist/templates/template_0000/src/style-options.js +0 -51
- package/dist/templates/template_0000/src/template-dna.js +0 -9
- package/dist/templates/template_0000/src/template.js +0 -1441
- package/templates/template_0000/README.md +0 -77
- package/templates/template_0000/SKILL.md +0 -225
- package/templates/template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/template_0000/composition.json +0 -11
- package/templates/template_0000/package.json +0 -28
- package/templates/template_0000/research/preview/.gitkeep +0 -1
- package/templates/template_0000/research/source_notes.md +0 -7
- package/templates/template_0000/src/lib/images.js +0 -202
- package/templates/template_0000/src/lib/images.ts +0 -241
- package/templates/template_0000/src/remotion/Root.js +0 -33
- package/templates/template_0000/src/remotion/Root.tsx +0 -75
- package/templates/template_0000/src/remotion/index.js +0 -3
- package/templates/template_0000/src/remotion/index.tsx +0 -4
- package/templates/template_0000/src/sdk.js +0 -3
- package/templates/template_0000/src/sdk.ts +0 -122
- package/templates/template_0000/src/style-options.js +0 -51
- package/templates/template_0000/src/style-options.ts +0 -60
- package/templates/template_0000/src/template-dna.js +0 -9
- package/templates/template_0000/src/template-dna.ts +0 -15
- package/templates/template_0000/src/template.js +0 -1441
- package/templates/template_0000/src/template.ts +0 -2042
- package/templates/template_0000/template.config.json +0 -21
- package/templates/template_0000/tsconfig.json +0 -19
package/dist/src/db.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import { config } from "./config.js";
|
|
3
|
+
import { createId } from "./lib/ids.js";
|
|
3
4
|
import { parseJson, stringifyJson } from "./lib/json.js";
|
|
4
5
|
import { nowIso } from "./lib/time.js";
|
|
5
6
|
const db = new Database(config.VIDFARM_DB_PATH);
|
|
@@ -13,6 +14,7 @@ create table if not exists customers (
|
|
|
13
14
|
default_webhook_url text,
|
|
14
15
|
is_developer integer not null default 0,
|
|
15
16
|
is_paid_plan integer not null default 0,
|
|
17
|
+
plan_tier text not null default 'starter',
|
|
16
18
|
about text,
|
|
17
19
|
groupchat_url text,
|
|
18
20
|
flockposter_api_key text,
|
|
@@ -49,7 +51,6 @@ create table if not exists customer_provider_keys (
|
|
|
49
51
|
label text,
|
|
50
52
|
encrypted_secret text not null,
|
|
51
53
|
status text not null,
|
|
52
|
-
weight integer not null default 1,
|
|
53
54
|
last_used_at text,
|
|
54
55
|
cooldown_until text,
|
|
55
56
|
disabled_reason text,
|
|
@@ -66,6 +67,19 @@ create table if not exists provider_key_leases (
|
|
|
66
67
|
expires_at text not null
|
|
67
68
|
);
|
|
68
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
|
+
|
|
69
83
|
create table if not exists provider_key_usage_events (
|
|
70
84
|
id text primary key,
|
|
71
85
|
key_id text not null references customer_provider_keys(id) on delete cascade,
|
|
@@ -128,18 +142,20 @@ create table if not exists artifacts (
|
|
|
128
142
|
created_at text not null
|
|
129
143
|
);
|
|
130
144
|
|
|
131
|
-
|
|
145
|
+
drop table if exists billing_events;
|
|
146
|
+
|
|
147
|
+
create table if not exists customer_credit_grants (
|
|
132
148
|
id text primary key,
|
|
133
149
|
customer_id text not null references customers(id) on delete cascade,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
type text not null,
|
|
137
|
-
cost_usd real not null,
|
|
138
|
-
charge_usd real not null,
|
|
150
|
+
grant_type text not null,
|
|
151
|
+
amount_usd real not null,
|
|
139
152
|
metadata_json text not null,
|
|
140
|
-
created_at text not null
|
|
153
|
+
created_at text not null,
|
|
154
|
+
unique(customer_id, grant_type)
|
|
141
155
|
);
|
|
142
156
|
|
|
157
|
+
create index if not exists idx_customer_credit_grants_customer_created on customer_credit_grants(customer_id, created_at desc);
|
|
158
|
+
|
|
143
159
|
create table if not exists webhook_deliveries (
|
|
144
160
|
id text primary key,
|
|
145
161
|
job_id text not null references jobs(id) on delete cascade,
|
|
@@ -168,6 +184,110 @@ create table if not exists user_attachments (
|
|
|
168
184
|
|
|
169
185
|
create index if not exists idx_user_attachments_customer_created on user_attachments(customer_id, created_at desc);
|
|
170
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
|
+
|
|
171
291
|
create table if not exists template_sources (
|
|
172
292
|
id text primary key,
|
|
173
293
|
template_id text not null unique,
|
|
@@ -192,6 +312,8 @@ create table if not exists template_releases (
|
|
|
192
312
|
checkout_path text not null,
|
|
193
313
|
skill_path text not null,
|
|
194
314
|
module_path text not null,
|
|
315
|
+
artifact_manifest_key text,
|
|
316
|
+
artifact_key text,
|
|
195
317
|
status text not null,
|
|
196
318
|
certification_report_json text,
|
|
197
319
|
activated_at text,
|
|
@@ -209,6 +331,16 @@ if (!customerColumns.some((column) => column.name === "is_developer")) {
|
|
|
209
331
|
if (!customerColumns.some((column) => column.name === "is_paid_plan")) {
|
|
210
332
|
db.exec(`alter table customers add column is_paid_plan integer not null default 0;`);
|
|
211
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
|
+
`);
|
|
212
344
|
if (!customerColumns.some((column) => column.name === "password_hash")) {
|
|
213
345
|
db.exec(`alter table customers add column password_hash text;`);
|
|
214
346
|
}
|
|
@@ -225,6 +357,10 @@ const apiKeyColumns = db.prepare(`pragma table_info(api_keys)`).all();
|
|
|
225
357
|
if (!apiKeyColumns.some((column) => column.name === "raw_value")) {
|
|
226
358
|
db.exec(`alter table api_keys add column raw_value text;`);
|
|
227
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
|
+
}
|
|
228
364
|
const templateSourceColumns = db.prepare(`pragma table_info(template_sources)`).all();
|
|
229
365
|
if (!templateSourceColumns.some((column) => column.name === "slug_id")) {
|
|
230
366
|
db.exec(`alter table template_sources add column slug_id text;`);
|
|
@@ -245,6 +381,31 @@ db.exec(`
|
|
|
245
381
|
set status = 'pending_approval'
|
|
246
382
|
where status = 'certified';
|
|
247
383
|
`);
|
|
384
|
+
const templateReleaseColumns = db.prepare(`pragma table_info(template_releases)`).all();
|
|
385
|
+
if (!templateReleaseColumns.some((column) => column.name === "artifact_manifest_key")) {
|
|
386
|
+
db.exec(`alter table template_releases add column artifact_manifest_key text;`);
|
|
387
|
+
}
|
|
388
|
+
if (!templateReleaseColumns.some((column) => column.name === "artifact_key")) {
|
|
389
|
+
db.exec(`alter table template_releases add column artifact_key text;`);
|
|
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
|
+
}
|
|
248
409
|
db.exec(`drop index if exists idx_job_events_job_time;`);
|
|
249
410
|
db.exec(`drop table if exists job_events;`);
|
|
250
411
|
function mapJob(row) {
|
|
@@ -298,6 +459,8 @@ function mapTemplateRelease(row) {
|
|
|
298
459
|
checkoutPath: String(row.checkout_path),
|
|
299
460
|
skillPath: String(row.skill_path),
|
|
300
461
|
modulePath: String(row.module_path),
|
|
462
|
+
artifactManifestKey: row.artifact_manifest_key ? String(row.artifact_manifest_key) : null,
|
|
463
|
+
artifactKey: row.artifact_key ? String(row.artifact_key) : null,
|
|
301
464
|
status: row.status,
|
|
302
465
|
certificationReport: parseJson(row.certification_report_json, null),
|
|
303
466
|
activatedAt: row.activated_at ? String(row.activated_at) : null,
|
|
@@ -317,6 +480,101 @@ function mapUserAttachment(row) {
|
|
|
317
480
|
createdAt: String(row.created_at)
|
|
318
481
|
};
|
|
319
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
|
+
}
|
|
320
578
|
export const database = {
|
|
321
579
|
raw: db,
|
|
322
580
|
upsertCustomer(input) {
|
|
@@ -325,9 +583,11 @@ export const database = {
|
|
|
325
583
|
db.prepare(`
|
|
326
584
|
insert into customers (
|
|
327
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
|
|
328
587
|
)
|
|
329
588
|
values (
|
|
330
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
|
|
331
591
|
)
|
|
332
592
|
on conflict(id) do update set
|
|
333
593
|
email = excluded.email,
|
|
@@ -335,6 +595,7 @@ export const database = {
|
|
|
335
595
|
default_webhook_url = excluded.default_webhook_url,
|
|
336
596
|
is_developer = excluded.is_developer,
|
|
337
597
|
is_paid_plan = excluded.is_paid_plan,
|
|
598
|
+
plan_tier = excluded.plan_tier,
|
|
338
599
|
about = excluded.about,
|
|
339
600
|
groupchat_url = excluded.groupchat_url,
|
|
340
601
|
flockposter_api_key = excluded.flockposter_api_key,
|
|
@@ -347,6 +608,7 @@ export const database = {
|
|
|
347
608
|
default_webhook_url: input.defaultWebhookUrl ?? null,
|
|
348
609
|
is_developer: input.isDeveloper === undefined ? Number(current?.is_developer ?? 0) : (input.isDeveloper ? 1 : 0),
|
|
349
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,
|
|
350
612
|
about: input.about === undefined ? (current?.about ?? null) : input.about,
|
|
351
613
|
groupchat_url: input.groupchatUrl === undefined ? (current?.groupchat_url ?? null) : input.groupchatUrl,
|
|
352
614
|
flockposter_api_key: input.flockposterApiKey === undefined ? (current?.flockposter_api_key ?? null) : input.flockposterApiKey,
|
|
@@ -368,18 +630,42 @@ export const database = {
|
|
|
368
630
|
defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null,
|
|
369
631
|
isDeveloper: Boolean(row.is_developer),
|
|
370
632
|
isPaidPlan: Boolean(row.is_paid_plan),
|
|
633
|
+
planTier: normalizeCustomerPlanTier(row.plan_tier),
|
|
371
634
|
about: row.about ? String(row.about) : null,
|
|
372
635
|
groupchatUrl: row.groupchat_url ? String(row.groupchat_url) : null,
|
|
373
|
-
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)
|
|
374
639
|
};
|
|
375
640
|
},
|
|
376
641
|
getCustomerByEmail(email) {
|
|
377
642
|
const row = db.prepare(`select * from customers where lower(email) = lower(?)`).get(email);
|
|
378
643
|
return row ? this.getCustomerById(String(row.id)) : null;
|
|
379
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
|
+
},
|
|
380
666
|
getCustomerAuthByEmail(email) {
|
|
381
667
|
return db.prepare(`
|
|
382
|
-
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
|
|
383
669
|
, about, groupchat_url, flockposter_api_key
|
|
384
670
|
from customers
|
|
385
671
|
where lower(email) = lower(?)
|
|
@@ -393,23 +679,64 @@ export const database = {
|
|
|
393
679
|
where id = ?
|
|
394
680
|
`).run(passwordHash, nowIso(), customerId);
|
|
395
681
|
},
|
|
396
|
-
updateCustomerPaidPlan(customerId, isPaidPlan) {
|
|
682
|
+
updateCustomerPaidPlan(customerId, isPaidPlan, planTier) {
|
|
683
|
+
const existing = this.getCustomerById(customerId);
|
|
397
684
|
db.prepare(`
|
|
398
685
|
update customers
|
|
399
|
-
set is_paid_plan = ?, updated_at = ?
|
|
686
|
+
set is_paid_plan = ?, plan_tier = ?, updated_at = ?
|
|
400
687
|
where id = ?
|
|
401
|
-
`).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
|
+
});
|
|
712
|
+
},
|
|
713
|
+
setCustomerAccessByEmail(input) {
|
|
714
|
+
const normalizedEmail = input.email.trim().toLowerCase();
|
|
715
|
+
const existing = this.getCustomerByEmail(normalizedEmail);
|
|
716
|
+
return this.upsertCustomer({
|
|
717
|
+
id: existing?.id ?? createId("cus"),
|
|
718
|
+
email: normalizedEmail,
|
|
719
|
+
name: input.name === undefined ? (existing?.name ?? null) : input.name,
|
|
720
|
+
defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
|
|
721
|
+
isDeveloper: input.isDeveloper === undefined ? (existing?.isDeveloper ?? false) : input.isDeveloper,
|
|
722
|
+
isPaidPlan: input.isPaidPlan === undefined ? (existing?.isPaidPlan ?? false) : input.isPaidPlan,
|
|
723
|
+
planTier: input.planTier === undefined ? (existing?.planTier ?? "starter") : input.planTier,
|
|
724
|
+
about: existing?.about ?? null,
|
|
725
|
+
groupchatUrl: existing?.groupchatUrl ?? null,
|
|
726
|
+
flockposterApiKey: existing?.flockposterApiKey ?? null
|
|
727
|
+
});
|
|
402
728
|
},
|
|
403
729
|
updateCustomerProfile(input) {
|
|
730
|
+
const existing = this.getCustomerById(input.customerId);
|
|
404
731
|
db.prepare(`
|
|
405
732
|
update customers
|
|
406
|
-
set about = ?, groupchat_url = ?, flockposter_api_key = ?, updated_at = ?
|
|
733
|
+
set name = ?, about = ?, groupchat_url = ?, flockposter_api_key = ?, updated_at = ?
|
|
407
734
|
where id = ?
|
|
408
|
-
`).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);
|
|
409
736
|
},
|
|
410
737
|
listProviderKeys(customerId) {
|
|
411
738
|
return db.prepare(`
|
|
412
|
-
select id, provider, label, status,
|
|
739
|
+
select id, provider, label, status, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
|
|
413
740
|
from customer_provider_keys
|
|
414
741
|
where customer_id = ?
|
|
415
742
|
order by provider asc, created_at desc
|
|
@@ -417,7 +744,7 @@ export const database = {
|
|
|
417
744
|
},
|
|
418
745
|
listProviderKeysWithSecrets(customerId) {
|
|
419
746
|
return db.prepare(`
|
|
420
|
-
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
|
|
421
748
|
from customer_provider_keys
|
|
422
749
|
where customer_id = ?
|
|
423
750
|
order by provider asc, created_at desc
|
|
@@ -426,6 +753,115 @@ export const database = {
|
|
|
426
753
|
deleteProviderKey(customerId, keyId) {
|
|
427
754
|
db.prepare(`delete from customer_provider_keys where id = ? and customer_id = ?`).run(keyId, customerId);
|
|
428
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
|
+
},
|
|
429
865
|
insertOtpChallenge(record) {
|
|
430
866
|
db.prepare(`
|
|
431
867
|
insert into otp_challenges (id, email, code_hash, expires_at, created_at)
|
|
@@ -475,7 +911,7 @@ export const database = {
|
|
|
475
911
|
},
|
|
476
912
|
findApiKeyByHash(keyHash) {
|
|
477
913
|
return db.prepare(`
|
|
478
|
-
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
|
|
479
915
|
from api_keys a
|
|
480
916
|
join customers c on c.id = a.customer_id
|
|
481
917
|
where a.key_hash = ? and a.status = 'active'
|
|
@@ -550,6 +986,17 @@ export const database = {
|
|
|
550
986
|
const row = db.prepare(`select * from jobs where id = ?`).get(id);
|
|
551
987
|
return row ? mapJob(row) : null;
|
|
552
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
|
+
},
|
|
553
1000
|
countJobsByStatuses(input) {
|
|
554
1001
|
const statuses = input.statuses.length ? input.statuses : ["queued"];
|
|
555
1002
|
const filters = [`status in (${statuses.map(() => "?").join(", ")})`];
|
|
@@ -574,11 +1021,37 @@ export const database = {
|
|
|
574
1021
|
`).all(nowIso(), limit);
|
|
575
1022
|
return rows.map(mapJob);
|
|
576
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
|
+
},
|
|
577
1045
|
markJobRunning(id) {
|
|
578
1046
|
const timestamp = nowIso();
|
|
579
1047
|
db.prepare(`
|
|
580
1048
|
update jobs
|
|
581
|
-
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
|
|
582
1055
|
where id = ?
|
|
583
1056
|
`).run(timestamp, timestamp, id);
|
|
584
1057
|
},
|
|
@@ -624,15 +1097,43 @@ export const database = {
|
|
|
624
1097
|
filters.push("created_at <= ?");
|
|
625
1098
|
values.push(input.endTime);
|
|
626
1099
|
}
|
|
627
|
-
|
|
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)));
|
|
628
1105
|
const rows = db.prepare(`
|
|
629
1106
|
select * from jobs
|
|
630
1107
|
where ${filters.join(" and ")}
|
|
631
|
-
order by created_at desc
|
|
1108
|
+
order by created_at desc, id desc
|
|
632
1109
|
limit ?
|
|
633
1110
|
`).all(...values);
|
|
634
1111
|
return rows.map(mapJob);
|
|
635
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
|
+
},
|
|
636
1137
|
insertArtifact(record) {
|
|
637
1138
|
db.prepare(`
|
|
638
1139
|
insert into artifacts (id, job_id, customer_id, template_id, kind, storage_key, public_url, metadata_json, created_at)
|
|
@@ -649,21 +1150,26 @@ export const database = {
|
|
|
649
1150
|
created_at: nowIso()
|
|
650
1151
|
});
|
|
651
1152
|
},
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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);
|
|
667
1173
|
},
|
|
668
1174
|
queueWebhookDelivery(record) {
|
|
669
1175
|
const timestamp = nowIso();
|
|
@@ -716,6 +1222,28 @@ export const database = {
|
|
|
716
1222
|
acquireProviderKeyLease(input) {
|
|
717
1223
|
const transaction = db.transaction(() => {
|
|
718
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
|
+
}
|
|
719
1247
|
const row = db.prepare(`
|
|
720
1248
|
select k.id, k.encrypted_secret
|
|
721
1249
|
from customer_provider_keys k
|
|
@@ -725,7 +1253,7 @@ export const database = {
|
|
|
725
1253
|
and k.status = 'active'
|
|
726
1254
|
and (k.cooldown_until is null or k.cooldown_until <= ?)
|
|
727
1255
|
and l.key_id is null
|
|
728
|
-
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
|
|
729
1257
|
limit 1
|
|
730
1258
|
`).get(nowIso(), input.customerId, input.provider, nowIso());
|
|
731
1259
|
if (!row) {
|
|
@@ -735,10 +1263,14 @@ export const database = {
|
|
|
735
1263
|
insert into provider_key_leases (key_id, lease_token, worker_id, job_id, leased_at, expires_at)
|
|
736
1264
|
values (?, ?, ?, ?, ?, ?)
|
|
737
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);
|
|
738
1267
|
return { keyId: row.id, encryptedSecret: row.encrypted_secret };
|
|
739
1268
|
});
|
|
740
1269
|
return transaction();
|
|
741
1270
|
},
|
|
1271
|
+
deleteProviderKeyWaiter(waiterId) {
|
|
1272
|
+
db.prepare(`delete from provider_key_waiters where id = ?`).run(waiterId);
|
|
1273
|
+
},
|
|
742
1274
|
releaseProviderKeyLease(input) {
|
|
743
1275
|
db.prepare(`delete from provider_key_leases where key_id = ? and lease_token = ?`).run(input.keyId, input.leaseToken);
|
|
744
1276
|
},
|
|
@@ -777,9 +1309,9 @@ export const database = {
|
|
|
777
1309
|
const timestamp = nowIso();
|
|
778
1310
|
db.prepare(`
|
|
779
1311
|
insert into customer_provider_keys (
|
|
780
|
-
id, customer_id, provider, label, encrypted_secret, status,
|
|
1312
|
+
id, customer_id, provider, label, encrypted_secret, status, created_at, updated_at
|
|
781
1313
|
) values (
|
|
782
|
-
@id, @customer_id, @provider, @label, @encrypted_secret, 'active', @
|
|
1314
|
+
@id, @customer_id, @provider, @label, @encrypted_secret, 'active', @created_at, @updated_at
|
|
783
1315
|
)
|
|
784
1316
|
`).run({
|
|
785
1317
|
id: record.id,
|
|
@@ -787,7 +1319,6 @@ export const database = {
|
|
|
787
1319
|
provider: record.provider,
|
|
788
1320
|
label: record.label,
|
|
789
1321
|
encrypted_secret: record.encryptedSecret,
|
|
790
|
-
weight: record.weight,
|
|
791
1322
|
created_at: timestamp,
|
|
792
1323
|
updated_at: timestamp
|
|
793
1324
|
});
|
|
@@ -831,6 +1362,281 @@ export const database = {
|
|
|
831
1362
|
deleteUserAttachment(customerId, attachmentId) {
|
|
832
1363
|
db.prepare(`delete from user_attachments where customer_id = ? and id = ?`).run(customerId, attachmentId);
|
|
833
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
|
+
},
|
|
834
1640
|
createTemplateSource(record) {
|
|
835
1641
|
const timestamp = nowIso();
|
|
836
1642
|
db.prepare(`
|
|
@@ -905,26 +1711,42 @@ export const database = {
|
|
|
905
1711
|
`).get(repoUrl, branch, templateModulePath);
|
|
906
1712
|
return row ? mapTemplateSource(row) : null;
|
|
907
1713
|
},
|
|
908
|
-
listTemplateSources(
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
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));
|
|
912
1732
|
return rows.map(mapTemplateSource);
|
|
913
1733
|
},
|
|
914
1734
|
createTemplateRelease(record) {
|
|
915
1735
|
const timestamp = nowIso();
|
|
916
1736
|
db.prepare(`
|
|
917
1737
|
insert into template_releases (
|
|
918
|
-
id, source_id, template_id, branch, commit_sha, checkout_path, skill_path, module_path, status,
|
|
1738
|
+
id, source_id, template_id, branch, commit_sha, checkout_path, skill_path, module_path, artifact_manifest_key, artifact_key, status,
|
|
919
1739
|
certification_report_json, activated_at, created_at, updated_at
|
|
920
1740
|
) values (
|
|
921
|
-
@id, @source_id, @template_id, @branch, @commit_sha, @checkout_path, @skill_path, @module_path, @status,
|
|
1741
|
+
@id, @source_id, @template_id, @branch, @commit_sha, @checkout_path, @skill_path, @module_path, @artifact_manifest_key, @artifact_key, @status,
|
|
922
1742
|
@certification_report_json, @activated_at, @created_at, @updated_at
|
|
923
1743
|
)
|
|
924
1744
|
on conflict(source_id, commit_sha) do update set
|
|
925
1745
|
checkout_path = excluded.checkout_path,
|
|
926
1746
|
skill_path = excluded.skill_path,
|
|
927
1747
|
module_path = excluded.module_path,
|
|
1748
|
+
artifact_manifest_key = excluded.artifact_manifest_key,
|
|
1749
|
+
artifact_key = excluded.artifact_key,
|
|
928
1750
|
status = excluded.status,
|
|
929
1751
|
certification_report_json = excluded.certification_report_json,
|
|
930
1752
|
activated_at = excluded.activated_at,
|
|
@@ -938,6 +1760,8 @@ export const database = {
|
|
|
938
1760
|
checkout_path: record.checkoutPath,
|
|
939
1761
|
skill_path: record.skillPath,
|
|
940
1762
|
module_path: record.modulePath,
|
|
1763
|
+
artifact_manifest_key: record.artifactManifestKey ?? null,
|
|
1764
|
+
artifact_key: record.artifactKey ?? null,
|
|
941
1765
|
status: record.status,
|
|
942
1766
|
certification_report_json: stringifyJson(record.certificationReport ?? null),
|
|
943
1767
|
activated_at: record.activatedAt ?? null,
|
|
@@ -965,15 +1789,27 @@ export const database = {
|
|
|
965
1789
|
return row ? mapTemplateRelease(row) : null;
|
|
966
1790
|
},
|
|
967
1791
|
listTemplateReleases(input) {
|
|
968
|
-
const
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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));
|
|
977
1813
|
return rows.map(mapTemplateRelease);
|
|
978
1814
|
},
|
|
979
1815
|
getActiveTemplateReleases() {
|
|
@@ -1009,5 +1845,163 @@ export const database = {
|
|
|
1009
1845
|
updated_at: nowIso(),
|
|
1010
1846
|
template_id: templateId
|
|
1011
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;
|
|
1012
2000
|
}
|
|
1013
2001
|
};
|
|
2002
|
+
function normalizeCustomerPlanTier(value) {
|
|
2003
|
+
if (value === "starter" || value === "standard" || value === "agency") {
|
|
2004
|
+
return value;
|
|
2005
|
+
}
|
|
2006
|
+
return "starter";
|
|
2007
|
+
}
|