@cogenta/schema 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/define-collection.d.ts.map +1 -1
- package/dist/define-collection.js +53 -3
- package/dist/define-collection.js.map +1 -1
- package/dist/generate-types.d.ts.map +1 -1
- package/dist/generate-types.js +6 -1
- package/dist/generate-types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/media-usage.d.ts +56 -0
- package/dist/media-usage.d.ts.map +1 -0
- package/dist/media-usage.js +89 -0
- package/dist/media-usage.js.map +1 -0
- package/dist/rich-text.d.ts +28 -2
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +26 -3
- package/dist/rich-text.js.map +1 -1
- package/dist/routing/index.d.ts +5 -1
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +2 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/not-found-log.d.ts +78 -0
- package/dist/routing/not-found-log.d.ts.map +1 -0
- package/dist/routing/not-found-log.js +153 -0
- package/dist/routing/not-found-log.js.map +1 -0
- package/dist/routing/redirect-patterns.d.ts +61 -0
- package/dist/routing/redirect-patterns.d.ts.map +1 -0
- package/dist/routing/redirect-patterns.js +173 -0
- package/dist/routing/redirect-patterns.js.map +1 -0
- package/dist/routing/redirects.d.ts +27 -4
- package/dist/routing/redirects.d.ts.map +1 -1
- package/dist/routing/redirects.js +160 -48
- package/dist/routing/redirects.js.map +1 -1
- package/dist/scheduling/index.d.ts +2 -0
- package/dist/scheduling/index.d.ts.map +1 -1
- package/dist/scheduling/index.js +1 -0
- package/dist/scheduling/index.js.map +1 -1
- package/dist/scheduling/registry.d.ts +147 -0
- package/dist/scheduling/registry.d.ts.map +1 -0
- package/dist/scheduling/registry.js +318 -0
- package/dist/scheduling/registry.js.map +1 -0
- package/dist/search/extract.d.ts +24 -0
- package/dist/search/extract.d.ts.map +1 -1
- package/dist/search/extract.js +63 -8
- package/dist/search/extract.js.map +1 -1
- package/dist/search/index.d.ts +2 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -9
- package/dist/search/index.js.map +1 -1
- package/dist/store/admin-theme-store.d.ts +26 -0
- package/dist/store/admin-theme-store.d.ts.map +1 -0
- package/dist/store/admin-theme-store.js +81 -0
- package/dist/store/admin-theme-store.js.map +1 -0
- package/dist/store/admin-theme-tables.d.ts +24 -0
- package/dist/store/admin-theme-tables.d.ts.map +1 -0
- package/dist/store/admin-theme-tables.js +41 -0
- package/dist/store/admin-theme-tables.js.map +1 -0
- package/dist/store/admin-theme-templates.d.ts +135 -0
- package/dist/store/admin-theme-templates.d.ts.map +1 -0
- package/dist/store/admin-theme-templates.js +217 -0
- package/dist/store/admin-theme-templates.js.map +1 -0
- package/dist/store/columns.d.ts.map +1 -1
- package/dist/store/columns.js +8 -2
- package/dist/store/columns.js.map +1 -1
- package/dist/store/diff.d.ts +40 -0
- package/dist/store/diff.d.ts.map +1 -1
- package/dist/store/diff.js +148 -0
- package/dist/store/diff.js.map +1 -1
- package/dist/store/index.d.ts +47 -4
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +26 -2
- package/dist/store/index.js.map +1 -1
- package/dist/store/maintenance-store.d.ts +34 -0
- package/dist/store/maintenance-store.d.ts.map +1 -0
- package/dist/store/maintenance-store.js +92 -0
- package/dist/store/maintenance-store.js.map +1 -0
- package/dist/store/menu-store.d.ts +146 -0
- package/dist/store/menu-store.d.ts.map +1 -0
- package/dist/store/menu-store.js +565 -0
- package/dist/store/menu-store.js.map +1 -0
- package/dist/store/menu-tables.d.ts +38 -0
- package/dist/store/menu-tables.d.ts.map +1 -0
- package/dist/store/menu-tables.js +117 -0
- package/dist/store/menu-tables.js.map +1 -0
- package/dist/store/naming.d.ts +1 -1
- package/dist/store/naming.d.ts.map +1 -1
- package/dist/store/naming.js +2 -0
- package/dist/store/naming.js.map +1 -1
- package/dist/store/pattern-store.d.ts +75 -0
- package/dist/store/pattern-store.d.ts.map +1 -0
- package/dist/store/pattern-store.js +118 -0
- package/dist/store/pattern-store.js.map +1 -0
- package/dist/store/pattern-tables.d.ts +29 -0
- package/dist/store/pattern-tables.d.ts.map +1 -0
- package/dist/store/pattern-tables.js +53 -0
- package/dist/store/pattern-tables.js.map +1 -0
- package/dist/store/read-only.d.ts +3 -3
- package/dist/store/read-only.d.ts.map +1 -1
- package/dist/store/read-only.js +4 -3
- package/dist/store/read-only.js.map +1 -1
- package/dist/store/redirect-tracking.d.ts +50 -0
- package/dist/store/redirect-tracking.d.ts.map +1 -0
- package/dist/store/redirect-tracking.js +91 -0
- package/dist/store/redirect-tracking.js.map +1 -0
- package/dist/store/review-transitions.d.ts +34 -0
- package/dist/store/review-transitions.d.ts.map +1 -0
- package/dist/store/review-transitions.js +51 -0
- package/dist/store/review-transitions.js.map +1 -0
- package/dist/store/role-permission-export.d.ts +22 -0
- package/dist/store/role-permission-export.d.ts.map +1 -0
- package/dist/store/role-permission-export.js +0 -0
- package/dist/store/role-permission-export.js.map +1 -0
- package/dist/store/role-permission-overlay.d.ts +27 -0
- package/dist/store/role-permission-overlay.d.ts.map +1 -0
- package/dist/store/role-permission-overlay.js +0 -0
- package/dist/store/role-permission-overlay.js.map +1 -0
- package/dist/store/role-permission-store.d.ts +72 -0
- package/dist/store/role-permission-store.d.ts.map +1 -0
- package/dist/store/role-permission-store.js +148 -0
- package/dist/store/role-permission-store.js.map +1 -0
- package/dist/store/role-permission-tables.d.ts +27 -0
- package/dist/store/role-permission-tables.d.ts.map +1 -0
- package/dist/store/role-permission-tables.js +52 -0
- package/dist/store/role-permission-tables.js.map +1 -0
- package/dist/store/scheduled-publish-enqueue.d.ts +37 -0
- package/dist/store/scheduled-publish-enqueue.d.ts.map +1 -0
- package/dist/store/scheduled-publish-enqueue.js +51 -0
- package/dist/store/scheduled-publish-enqueue.js.map +1 -0
- package/dist/store/scheduled-publish-failures.d.ts +36 -0
- package/dist/store/scheduled-publish-failures.d.ts.map +1 -0
- package/dist/store/scheduled-publish-failures.js +67 -0
- package/dist/store/scheduled-publish-failures.js.map +1 -0
- package/dist/store/schema-2-1-migration.d.ts +29 -0
- package/dist/store/schema-2-1-migration.d.ts.map +1 -0
- package/dist/store/schema-2-1-migration.js +71 -0
- package/dist/store/schema-2-1-migration.js.map +1 -0
- package/dist/store/search-console-store.d.ts +44 -0
- package/dist/store/search-console-store.d.ts.map +1 -0
- package/dist/store/search-console-store.js +90 -0
- package/dist/store/search-console-store.js.map +1 -0
- package/dist/store/search-console-tables.d.ts +14 -0
- package/dist/store/search-console-tables.d.ts.map +1 -0
- package/dist/store/search-console-tables.js +29 -0
- package/dist/store/search-console-tables.js.map +1 -0
- package/dist/store/search-indexing.d.ts +36 -0
- package/dist/store/search-indexing.d.ts.map +1 -1
- package/dist/store/search-indexing.js +33 -0
- package/dist/store/search-indexing.js.map +1 -1
- package/dist/store/site-settings-registry.d.ts +164 -0
- package/dist/store/site-settings-registry.d.ts.map +1 -0
- package/dist/store/site-settings-registry.js +1160 -0
- package/dist/store/site-settings-registry.js.map +1 -0
- package/dist/store/site-settings-store.d.ts +42 -0
- package/dist/store/site-settings-store.d.ts.map +1 -0
- package/dist/store/site-settings-store.js +97 -0
- package/dist/store/site-settings-store.js.map +1 -0
- package/dist/store/site-settings-tables.d.ts +29 -0
- package/dist/store/site-settings-tables.d.ts.map +1 -0
- package/dist/store/site-settings-tables.js +48 -0
- package/dist/store/site-settings-tables.js.map +1 -0
- package/dist/store/store.d.ts +57 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +278 -37
- package/dist/store/store.js.map +1 -1
- package/dist/store/tables.d.ts.map +1 -1
- package/dist/store/tables.js +11 -0
- package/dist/store/tables.js.map +1 -1
- package/dist/store/taxonomy-store.d.ts.map +1 -1
- package/dist/store/taxonomy-store.js +90 -39
- package/dist/store/taxonomy-store.js.map +1 -1
- package/dist/store/taxonomy-usage.d.ts +47 -0
- package/dist/store/taxonomy-usage.d.ts.map +1 -0
- package/dist/store/taxonomy-usage.js +101 -0
- package/dist/store/taxonomy-usage.js.map +1 -0
- package/dist/store/theme-store.d.ts +73 -0
- package/dist/store/theme-store.d.ts.map +1 -0
- package/dist/store/theme-store.js +193 -0
- package/dist/store/theme-store.js.map +1 -0
- package/dist/store/types.d.ts +36 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/values.d.ts.map +1 -1
- package/dist/store/values.js +28 -1
- package/dist/store/values.js.map +1 -1
- package/dist/system-fields.d.ts +8 -1
- package/dist/system-fields.d.ts.map +1 -1
- package/dist/system-fields.js +14 -1
- package/dist/system-fields.js.map +1 -1
- package/dist/types.d.ts +54 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { CogentaError, identifier, limit, newId, sql, } from '@cogenta/core';
|
|
2
|
+
/**
|
|
3
|
+
* The scheduled-task registry (fiche 28 task 1).
|
|
4
|
+
*
|
|
5
|
+
* Before this, `cogenta serve` registered four unrelated pieces of recurring
|
|
6
|
+
* work — scheduled publication, the 404 log purge, the audit-integrity
|
|
7
|
+
* check, the trash sweep — each on its own bare `setInterval`, with no shared
|
|
8
|
+
* bookkeeping. Nothing recorded whether a run had happened, how long it took,
|
|
9
|
+
* or whether it had failed; the only way to know a scheduled publication had
|
|
10
|
+
* stopped working was a reader noticing a page never went live.
|
|
11
|
+
*
|
|
12
|
+
* A registered task declares a name, a description, an interval and a
|
|
13
|
+
* function. This module owns everything else: running due tasks, recording
|
|
14
|
+
* every run, and answering "what is scheduled, and did it work" for the
|
|
15
|
+
* admin screen fiche 28 task 2 builds over it.
|
|
16
|
+
*
|
|
17
|
+
* **Persistence is the point of task 3.** A task's "last run" is a row in
|
|
18
|
+
* this table, not a variable in the process — a `setInterval` handle dies
|
|
19
|
+
* with the process, and a site restarted after being down all night must be
|
|
20
|
+
* able to say, immediately, "the trash sweep has not run in fourteen hours"
|
|
21
|
+
* rather than resetting the clock the moment it comes back up. `tick()`
|
|
22
|
+
* itself is equally indifferent to *why* it is being called: `cogenta
|
|
23
|
+
* serve`'s own loop and the one-shot `cogenta cron` command both just call
|
|
24
|
+
* it, which is what makes the second a real alternative to the first rather
|
|
25
|
+
* than a separate reimplementation (fiche 28 task 5).
|
|
26
|
+
*/
|
|
27
|
+
export const SCHEDULED_TASK_RUNS_TABLE = 'cogenta_scheduled_task_runs';
|
|
28
|
+
/**
|
|
29
|
+
* One row per task: the compare-and-set lock `tick()` claims before running a
|
|
30
|
+
* task (L22 task 6). Kept separate from the run-history table above —
|
|
31
|
+
* history answers "what happened", this answers "is anyone already doing
|
|
32
|
+
* this right now", and conflating the two would make every read of the
|
|
33
|
+
* admin history screen reason about lock state.
|
|
34
|
+
*
|
|
35
|
+
* Before this table existed, `tick()` read the last run from the history
|
|
36
|
+
* table, decided a task was due, and only recorded the new run *after* it
|
|
37
|
+
* finished — nothing made that read-then-act atomic. With more than one
|
|
38
|
+
* `cogenta serve` replica (or a replica racing a `cogenta cron` invocation)
|
|
39
|
+
* against the same database, two processes could both read "due" and both
|
|
40
|
+
* start executing the same task, including the destructive trash-purge
|
|
41
|
+
* sweep. The fix is the same shape as `takeOne`'s guarded stock `UPDATE` in
|
|
42
|
+
* `@cogenta/commerce` and `claimOnce`'s guarded job `UPDATE` in
|
|
43
|
+
* `@cogenta/core`'s database queue: read `last_claim`, then issue a single
|
|
44
|
+
* `UPDATE ... WHERE last_claim = <the value just read>`. Only one of two
|
|
45
|
+
* racing `UPDATE`s can match that `WHERE` — the loser sees
|
|
46
|
+
* `rowsAffected === 0` and skips the task entirely. No dialect-specific
|
|
47
|
+
* locking primitive is needed: a single `UPDATE` with an equality `WHERE` is
|
|
48
|
+
* atomic per row on SQLite, Postgres and MySQL/MariaDB alike, so the exact
|
|
49
|
+
* same query runs unmodified on all three.
|
|
50
|
+
*/
|
|
51
|
+
export const SCHEDULED_TASK_CLAIMS_TABLE = 'cogenta_scheduled_task_claims';
|
|
52
|
+
/** Runs kept per task. Enough to see a pattern across a week of daily tasks, never unbounded. */
|
|
53
|
+
const DEFAULT_KEEP_RUNS = 50;
|
|
54
|
+
function toRun(row) {
|
|
55
|
+
return {
|
|
56
|
+
id: row.id,
|
|
57
|
+
taskName: row.task_name,
|
|
58
|
+
startedAt: Number(row.started_at),
|
|
59
|
+
finishedAt: Number(row.finished_at),
|
|
60
|
+
durationMs: Number(row.duration_ms),
|
|
61
|
+
outcome: row.outcome,
|
|
62
|
+
summary: row.summary,
|
|
63
|
+
error: row.error,
|
|
64
|
+
triggeredBy: row.triggered_by,
|
|
65
|
+
actor: row.actor,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function unknownTask(name) {
|
|
69
|
+
return new CogentaError({
|
|
70
|
+
code: 'SCHEDULER_TASK_UNKNOWN',
|
|
71
|
+
message: `No scheduled task named "${name}" is registered.`,
|
|
72
|
+
hint: 'Check the task name against GET /api/scheduled-tasks — a task only appears there once something has called register() for it.',
|
|
73
|
+
details: { name },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function createScheduledTaskRegistry(options) {
|
|
77
|
+
const { db } = options;
|
|
78
|
+
const now = options.now ?? Date.now;
|
|
79
|
+
const keepRuns = options.keepRuns ?? DEFAULT_KEEP_RUNS;
|
|
80
|
+
const logger = options.logger?.child({ component: 'scheduler' });
|
|
81
|
+
const table = identifier(options.table ?? SCHEDULED_TASK_RUNS_TABLE, db.dialect);
|
|
82
|
+
const claimsTable = identifier(options.claimsTable ?? SCHEDULED_TASK_CLAIMS_TABLE, db.dialect);
|
|
83
|
+
const definitions = new Map();
|
|
84
|
+
let ready = false;
|
|
85
|
+
async function ensureTable() {
|
|
86
|
+
if (ready)
|
|
87
|
+
return;
|
|
88
|
+
await db.query(sql `
|
|
89
|
+
create table if not exists ${table} (
|
|
90
|
+
id varchar(64) not null primary key,
|
|
91
|
+
task_name varchar(255) not null,
|
|
92
|
+
started_at bigint not null,
|
|
93
|
+
finished_at bigint not null,
|
|
94
|
+
duration_ms bigint not null,
|
|
95
|
+
outcome varchar(16) not null,
|
|
96
|
+
summary text,
|
|
97
|
+
error text,
|
|
98
|
+
triggered_by varchar(16) not null,
|
|
99
|
+
actor varchar(255)
|
|
100
|
+
)`);
|
|
101
|
+
await db
|
|
102
|
+
.query(sql `create index ${identifier('cogenta_scheduled_task_runs_task', db.dialect)}
|
|
103
|
+
on ${table} (task_name, started_at)`)
|
|
104
|
+
.catch(() => undefined); // already there
|
|
105
|
+
// The compare-and-set lock table (L22 task 6). `last_claim` starts out
|
|
106
|
+
// `null` — "never claimed" — for a brand-new row, matching the meaning
|
|
107
|
+
// `lastRunFor` gives a task that has never run.
|
|
108
|
+
await db.query(sql `
|
|
109
|
+
create table if not exists ${claimsTable} (
|
|
110
|
+
task_name varchar(255) not null primary key,
|
|
111
|
+
last_claim bigint
|
|
112
|
+
)`);
|
|
113
|
+
ready = true;
|
|
114
|
+
}
|
|
115
|
+
async function lastRunFor(name) {
|
|
116
|
+
await ensureTable();
|
|
117
|
+
const found = await db.query(sql `
|
|
118
|
+
select id, task_name, started_at, finished_at, duration_ms, outcome, summary, error, triggered_by, actor
|
|
119
|
+
from ${table}
|
|
120
|
+
where task_name = ${name}
|
|
121
|
+
order by started_at desc
|
|
122
|
+
limit ${limit(1)}`);
|
|
123
|
+
const row = found.rows[0];
|
|
124
|
+
return row === undefined ? null : toRun(row);
|
|
125
|
+
}
|
|
126
|
+
async function recentRunsFor(name) {
|
|
127
|
+
await ensureTable();
|
|
128
|
+
const found = await db.query(sql `
|
|
129
|
+
select id, task_name, started_at, finished_at, duration_ms, outcome, summary, error, triggered_by, actor
|
|
130
|
+
from ${table}
|
|
131
|
+
where task_name = ${name}
|
|
132
|
+
order by started_at desc
|
|
133
|
+
limit ${limit(keepRuns)}`);
|
|
134
|
+
return found.rows.map(toRun);
|
|
135
|
+
}
|
|
136
|
+
async function pruneOldRuns(name) {
|
|
137
|
+
// Keep the newest `keepRuns`, drop the rest — bounded history without a
|
|
138
|
+
// separate cleanup task of its own.
|
|
139
|
+
const kept = await db.query(sql `
|
|
140
|
+
select id from ${table}
|
|
141
|
+
where task_name = ${name}
|
|
142
|
+
order by started_at desc
|
|
143
|
+
limit ${limit(keepRuns)}`);
|
|
144
|
+
if (kept.rows.length < keepRuns)
|
|
145
|
+
return;
|
|
146
|
+
const keptIds = kept.rows.map((row) => row.id);
|
|
147
|
+
const inList = keptIds.map((id) => sql `${id}`).reduce((left, right) => sql `${left}, ${right}`);
|
|
148
|
+
await db.query(sql `delete from ${table} where task_name = ${name} and id not in (${inList})`);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Makes sure a claim row exists for a task, seeded from the run history so
|
|
152
|
+
* an upgrade from a database that already has runs (but no claims table
|
|
153
|
+
* yet) does not make every existing task look newly-never-run and fire
|
|
154
|
+
* immediately. A race between two processes both seeding the same row for
|
|
155
|
+
* the first time is harmless: the loser's `insert` hits the primary key
|
|
156
|
+
* and is swallowed, exactly like the "already there" index-creation guard
|
|
157
|
+
* above — the row itself, not who created it, is what matters.
|
|
158
|
+
*/
|
|
159
|
+
async function ensureClaimRow(name) {
|
|
160
|
+
await ensureTable();
|
|
161
|
+
const existing = await db.query(sql `select task_name from ${claimsTable} where task_name = ${name}`);
|
|
162
|
+
if (existing.rows.length > 0)
|
|
163
|
+
return;
|
|
164
|
+
const priorRun = await lastRunFor(name);
|
|
165
|
+
const seed = priorRun === null ? null : priorRun.startedAt;
|
|
166
|
+
await db
|
|
167
|
+
.query(sql `insert into ${claimsTable} (task_name, last_claim) values (${name}, ${seed})`)
|
|
168
|
+
.catch(() => undefined); // another process seeded it first — fine, the row exists either way
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The compare-and-set claim `tick()` takes before running a due task.
|
|
172
|
+
*
|
|
173
|
+
* Reads the task's current `last_claim`, decides whether that makes it
|
|
174
|
+
* due, and — only if so — tries to move `last_claim` forward with a single
|
|
175
|
+
* guarded `UPDATE` whose `WHERE` re-checks the exact value just read. If
|
|
176
|
+
* another process already claimed this task between the read and the
|
|
177
|
+
* write, `last_claim` no longer matches, `rowsAffected` comes back `0`,
|
|
178
|
+
* and this returns `false` — the caller must not run the task. Only one of
|
|
179
|
+
* two racing callers can ever see `true` for the same task and instant,
|
|
180
|
+
* because a single-row `UPDATE` with an equality `WHERE` is atomic on
|
|
181
|
+
* SQLite, Postgres and MySQL/MariaDB alike; nothing dialect-specific is
|
|
182
|
+
* needed here.
|
|
183
|
+
*/
|
|
184
|
+
async function tryClaim(definition, at) {
|
|
185
|
+
await ensureClaimRow(definition.name);
|
|
186
|
+
const found = await db.query(sql `select task_name, last_claim from ${claimsTable} where task_name = ${definition.name}`);
|
|
187
|
+
// Normalised the same way `toRun` normalises the run-history table's
|
|
188
|
+
// bigint columns: drivers disagree on whether a bigint column comes back
|
|
189
|
+
// as a JS number, a string or a `bigint`, and comparing/subtracting the
|
|
190
|
+
// wrong one silently produces `NaN` or a string mismatch rather than an
|
|
191
|
+
// error.
|
|
192
|
+
const raw = found.rows[0]?.last_claim;
|
|
193
|
+
const lastClaim = raw === null || raw === undefined ? null : Number(raw);
|
|
194
|
+
const due = lastClaim === null || at - lastClaim >= definition.intervalMs;
|
|
195
|
+
if (!due)
|
|
196
|
+
return false;
|
|
197
|
+
const result = lastClaim === null
|
|
198
|
+
? await db.query(sql `
|
|
199
|
+
update ${claimsTable} set last_claim = ${at}
|
|
200
|
+
where task_name = ${definition.name} and last_claim is null`)
|
|
201
|
+
: await db.query(sql `
|
|
202
|
+
update ${claimsTable} set last_claim = ${at}
|
|
203
|
+
where task_name = ${definition.name} and last_claim = ${lastClaim}`);
|
|
204
|
+
return result.rowsAffected > 0;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Keeps the claim row in step with the run it just recorded, whether the
|
|
208
|
+
* run was scheduled or manual (`runNow`) — a manual run must push the next
|
|
209
|
+
* scheduled run out exactly as it always has, since `tick()` used to base
|
|
210
|
+
* "is it due" on the run-history table, which every trigger writes to.
|
|
211
|
+
*/
|
|
212
|
+
async function upsertClaim(name, startedAt) {
|
|
213
|
+
await ensureClaimRow(name);
|
|
214
|
+
await db.query(sql `update ${claimsTable} set last_claim = ${startedAt} where task_name = ${name}`);
|
|
215
|
+
}
|
|
216
|
+
async function stateFor(definition) {
|
|
217
|
+
const lastRun = await lastRunFor(definition.name);
|
|
218
|
+
const at = now();
|
|
219
|
+
const nextRunAt = lastRun === null ? at : lastRun.startedAt + definition.intervalMs;
|
|
220
|
+
const overdue = lastRun === null ? true : at - lastRun.startedAt > definition.intervalMs * 2;
|
|
221
|
+
return {
|
|
222
|
+
name: definition.name,
|
|
223
|
+
description: definition.description,
|
|
224
|
+
intervalMs: definition.intervalMs,
|
|
225
|
+
destructive: definition.destructive ?? false,
|
|
226
|
+
lastRun,
|
|
227
|
+
nextRunAt,
|
|
228
|
+
overdue,
|
|
229
|
+
recentRuns: await recentRunsFor(definition.name),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function execute(definition, triggeredBy, actor) {
|
|
233
|
+
await ensureTable();
|
|
234
|
+
const startedAt = now();
|
|
235
|
+
let outcome = 'success';
|
|
236
|
+
let summary = null;
|
|
237
|
+
let error = null;
|
|
238
|
+
try {
|
|
239
|
+
const result = await definition.run();
|
|
240
|
+
summary = result?.summary ?? null;
|
|
241
|
+
}
|
|
242
|
+
catch (caught) {
|
|
243
|
+
outcome = 'error';
|
|
244
|
+
error = caught instanceof Error ? caught.message : String(caught);
|
|
245
|
+
logger?.error('scheduled task failed', { task: definition.name, error });
|
|
246
|
+
}
|
|
247
|
+
const finishedAt = now();
|
|
248
|
+
const run = {
|
|
249
|
+
id: newId(now),
|
|
250
|
+
taskName: definition.name,
|
|
251
|
+
startedAt,
|
|
252
|
+
finishedAt,
|
|
253
|
+
durationMs: Math.max(0, finishedAt - startedAt),
|
|
254
|
+
outcome,
|
|
255
|
+
summary,
|
|
256
|
+
error,
|
|
257
|
+
triggeredBy,
|
|
258
|
+
actor,
|
|
259
|
+
};
|
|
260
|
+
await db.query(sql `
|
|
261
|
+
insert into ${table}
|
|
262
|
+
(id, task_name, started_at, finished_at, duration_ms, outcome, summary, error, triggered_by, actor)
|
|
263
|
+
values
|
|
264
|
+
(${run.id}, ${run.taskName}, ${run.startedAt}, ${run.finishedAt}, ${run.durationMs},
|
|
265
|
+
${run.outcome}, ${run.summary}, ${run.error}, ${run.triggeredBy}, ${run.actor})`);
|
|
266
|
+
await pruneOldRuns(definition.name);
|
|
267
|
+
await upsertClaim(definition.name, run.startedAt);
|
|
268
|
+
return run;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
register: (definition) => {
|
|
272
|
+
if (definitions.has(definition.name)) {
|
|
273
|
+
throw new CogentaError({
|
|
274
|
+
code: 'SCHEDULER_TASK_DUPLICATE',
|
|
275
|
+
message: `A scheduled task named "${definition.name}" is already registered.`,
|
|
276
|
+
hint: 'Task names must be unique. Pick a different name, or remove the duplicate registration.',
|
|
277
|
+
details: { name: definition.name },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
definitions.set(definition.name, definition);
|
|
281
|
+
},
|
|
282
|
+
list: async () => {
|
|
283
|
+
const states = [];
|
|
284
|
+
for (const definition of definitions.values()) {
|
|
285
|
+
states.push(await stateFor(definition));
|
|
286
|
+
}
|
|
287
|
+
return states;
|
|
288
|
+
},
|
|
289
|
+
get: async (name) => {
|
|
290
|
+
const definition = definitions.get(name);
|
|
291
|
+
return definition === undefined ? null : stateFor(definition);
|
|
292
|
+
},
|
|
293
|
+
runNow: async (name, runOptions = {}) => {
|
|
294
|
+
const definition = definitions.get(name);
|
|
295
|
+
if (definition === undefined)
|
|
296
|
+
throw unknownTask(name);
|
|
297
|
+
return execute(definition, 'manual', runOptions.actor ?? null);
|
|
298
|
+
},
|
|
299
|
+
tick: async (at = now()) => {
|
|
300
|
+
const ran = [];
|
|
301
|
+
// Sequenced, not concurrent within this process: this may share one
|
|
302
|
+
// SQLite connection with whatever else calls `db.transaction`, which
|
|
303
|
+
// cannot have two writers open at the same instant. Across processes,
|
|
304
|
+
// `tryClaim` is what actually prevents two replicas from both running
|
|
305
|
+
// the same task — this loop alone would not (L22 task 6).
|
|
306
|
+
for (const definition of definitions.values()) {
|
|
307
|
+
const claimed = await tryClaim(definition, at);
|
|
308
|
+
if (!claimed)
|
|
309
|
+
continue;
|
|
310
|
+
await execute(definition, 'schedule', null);
|
|
311
|
+
ran.push(definition.name);
|
|
312
|
+
}
|
|
313
|
+
return { ran };
|
|
314
|
+
},
|
|
315
|
+
ensureTable,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/scheduling/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/search/extract.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { SearchDocument } from './types.js';
|
|
|
12
12
|
export declare function extractRichText(value: unknown): string;
|
|
13
13
|
/** The prose of one block. Its `data` is contract B, so its shape is open. */
|
|
14
14
|
export declare function extractBlockText(block: ContentBlock): string;
|
|
15
|
+
export declare function titleOf(collection: CollectionDefinition, entry: ContentEntry): string;
|
|
15
16
|
/**
|
|
16
17
|
* The indexable document for an entry.
|
|
17
18
|
*
|
|
@@ -22,4 +23,27 @@ export declare function extractBlockText(block: ContentBlock): string;
|
|
|
22
23
|
* twice in the text every engine ranks.
|
|
23
24
|
*/
|
|
24
25
|
export declare function searchDocumentFor(collection: CollectionDefinition, entry: ContentEntry): SearchDocument;
|
|
26
|
+
/** One run of characters inside an excerpt that matched a query term. */
|
|
27
|
+
export interface ExcerptMatch {
|
|
28
|
+
readonly start: number;
|
|
29
|
+
readonly end: number;
|
|
30
|
+
}
|
|
31
|
+
export interface Excerpt {
|
|
32
|
+
readonly text: string;
|
|
33
|
+
readonly matches: readonly ExcerptMatch[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A window of prose around the first query term found in `text`, with the
|
|
37
|
+
* offsets of every match **inside that window** — never inside the full text,
|
|
38
|
+
* which the caller has already thrown away by the time it renders anything.
|
|
39
|
+
*
|
|
40
|
+
* `text` is expected to be the *display* text — `SearchDocument.body`, which
|
|
41
|
+
* `searchDocumentFor` builds with `condense` but never `foldText` (only the
|
|
42
|
+
* three drivers fold it, at the point they hand it to their own engine, for
|
|
43
|
+
* matching). That is what lets an excerpt keep real casing and accents while
|
|
44
|
+
* still being found by a folded, prefix-matching query: each candidate word is
|
|
45
|
+
* folded here, once, only to decide whether it counts as a match — never to
|
|
46
|
+
* change what is shown (fiche 36, task 3).
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildExcerpt(text: string, tokens: readonly string[]): Excerpt;
|
|
25
49
|
//# sourceMappingURL=extract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../src/search/extract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAC/E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AA0EhD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA6BtD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAI5D;
|
|
1
|
+
{"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../src/search/extract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAC/E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AA0EhD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA6BtD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAI5D;AAyBD,wBAAgB,OAAO,CAAC,UAAU,EAAE,oBAAoB,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAerF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,oBAAoB,EAChC,KAAK,EAAE,YAAY,GAClB,cAAc,CA8BhB;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;CAC1C;AAYD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAmC7E"}
|
package/dist/search/extract.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { condense } from './text.js';
|
|
1
|
+
import { condense, foldText } from './text.js';
|
|
2
2
|
/**
|
|
3
3
|
* Turning an entry into the text that gets indexed.
|
|
4
4
|
*
|
|
@@ -126,14 +126,20 @@ function extractZones(zones) {
|
|
|
126
126
|
/**
|
|
127
127
|
* The field whose value labels a result.
|
|
128
128
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
129
|
+
* Fiche 01 ("Liste de contenu"), task 1: the same priority order
|
|
130
|
+
* `packages/admin/src/lib/entry-title.ts` uses for every other screen that
|
|
131
|
+
* names an entry, so a title reads the same in the collection list, the
|
|
132
|
+
* trash and here. A declared `text` field named `title`, `name` or `label`
|
|
133
|
+
* — in that priority order — and the first declared `text` field
|
|
134
|
+
* otherwise, which is what an editor sees at the top of the form.
|
|
132
135
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
136
|
+
const TITLE_FIELD_PRIORITY = ['title', 'name', 'label'];
|
|
137
|
+
export function titleOf(collection, entry) {
|
|
138
|
+
for (const name of TITLE_FIELD_PRIORITY) {
|
|
139
|
+
const field = collection.fields[name];
|
|
140
|
+
if (field?.kind !== 'text')
|
|
141
|
+
continue;
|
|
142
|
+
const value = entry.values[name];
|
|
137
143
|
if (typeof value === 'string' && value.length > 0)
|
|
138
144
|
return condense(value);
|
|
139
145
|
}
|
|
@@ -185,4 +191,53 @@ export function searchDocumentFor(collection, entry) {
|
|
|
185
191
|
body: condense(parts.join(' ')),
|
|
186
192
|
};
|
|
187
193
|
}
|
|
194
|
+
/** Letters and digits, the same boundary `tokenize` (`text.ts`) uses. */
|
|
195
|
+
const WORD_PATTERN = /[\p{L}\p{N}]+/gu;
|
|
196
|
+
/** Characters kept on each side of the first match — enough prose to show why a
|
|
197
|
+
* result matched without turning the result list into a wall of text. */
|
|
198
|
+
const EXCERPT_RADIUS = 80;
|
|
199
|
+
/** No match found: the caller still gets a real opening, not a raw cutoff of `body`. */
|
|
200
|
+
const EXCERPT_FALLBACK_LENGTH = 200;
|
|
201
|
+
/**
|
|
202
|
+
* A window of prose around the first query term found in `text`, with the
|
|
203
|
+
* offsets of every match **inside that window** — never inside the full text,
|
|
204
|
+
* which the caller has already thrown away by the time it renders anything.
|
|
205
|
+
*
|
|
206
|
+
* `text` is expected to be the *display* text — `SearchDocument.body`, which
|
|
207
|
+
* `searchDocumentFor` builds with `condense` but never `foldText` (only the
|
|
208
|
+
* three drivers fold it, at the point they hand it to their own engine, for
|
|
209
|
+
* matching). That is what lets an excerpt keep real casing and accents while
|
|
210
|
+
* still being found by a folded, prefix-matching query: each candidate word is
|
|
211
|
+
* folded here, once, only to decide whether it counts as a match — never to
|
|
212
|
+
* change what is shown (fiche 36, task 3).
|
|
213
|
+
*/
|
|
214
|
+
export function buildExcerpt(text, tokens) {
|
|
215
|
+
if (tokens.length === 0 || text.length === 0) {
|
|
216
|
+
return { text: text.slice(0, EXCERPT_FALLBACK_LENGTH), matches: [] };
|
|
217
|
+
}
|
|
218
|
+
const words = [];
|
|
219
|
+
for (const match of text.matchAll(WORD_PATTERN)) {
|
|
220
|
+
words.push({ start: match.index, end: match.index + match[0].length });
|
|
221
|
+
}
|
|
222
|
+
// The same rule the FTS5/tsquery/boolean-mode drivers all express: a term
|
|
223
|
+
// matches a word that starts with it, not only a word equal to it.
|
|
224
|
+
function isHit(word) {
|
|
225
|
+
const folded = foldText(text.slice(word.start, word.end));
|
|
226
|
+
return tokens.some((token) => folded.startsWith(token));
|
|
227
|
+
}
|
|
228
|
+
const hits = words.filter(isHit);
|
|
229
|
+
if (hits.length === 0) {
|
|
230
|
+
return { text: text.slice(0, EXCERPT_FALLBACK_LENGTH), matches: [] };
|
|
231
|
+
}
|
|
232
|
+
const anchor = hits[0];
|
|
233
|
+
const windowStart = Math.max(0, anchor.start - EXCERPT_RADIUS);
|
|
234
|
+
const windowEnd = Math.min(text.length, anchor.end + EXCERPT_RADIUS);
|
|
235
|
+
const prefix = windowStart > 0 ? '… ' : '';
|
|
236
|
+
const suffix = windowEnd < text.length ? ' …' : '';
|
|
237
|
+
const shift = prefix.length - windowStart;
|
|
238
|
+
const matches = hits
|
|
239
|
+
.filter((word) => word.start >= windowStart && word.end <= windowEnd)
|
|
240
|
+
.map((word) => ({ start: word.start + shift, end: word.end + shift }));
|
|
241
|
+
return { text: `${prefix}${text.slice(windowStart, windowEnd)}${suffix}`, matches };
|
|
242
|
+
}
|
|
188
243
|
//# sourceMappingURL=extract.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/search/extract.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/search/extract.ts"],"names":[],"mappings":"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"}
|
package/dist/search/index.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ import type { SearchDriver } from './types.js';
|
|
|
8
8
|
* not ranked at all. What every caller does know is that the locale and the
|
|
9
9
|
* state it asks for are the only ones it can be given.
|
|
10
10
|
*/
|
|
11
|
-
export {
|
|
11
|
+
export type { Excerpt, ExcerptMatch } from './extract.js';
|
|
12
|
+
export { buildExcerpt, extractBlockText, extractRichText, searchDocumentFor, titleOf, } from './extract.js';
|
|
12
13
|
export type { MysqlSearchOptions } from './mysql.js';
|
|
13
14
|
export { createMysqlSearch } from './mysql.js';
|
|
14
15
|
export type { PostgresSearchOptions } from './postgres.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAIjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;;;;GAOG;AAEH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAIjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;;;;GAOG;AAEH,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,OAAO,GACR,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACtE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACvF,YAAY,EACV,cAAc,EACd,YAAY,EACZ,SAAS,EACT,WAAW,EACX,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAe1F"}
|
package/dist/search/index.js
CHANGED
|
@@ -2,15 +2,7 @@ import { CogentaError } from '@cogenta/core';
|
|
|
2
2
|
import { createMysqlSearch } from './mysql.js';
|
|
3
3
|
import { createPostgresSearch } from './postgres.js';
|
|
4
4
|
import { createSqliteSearch } from './sqlite.js';
|
|
5
|
-
|
|
6
|
-
* Full-text search, one interface over three engines (L1, task 16).
|
|
7
|
-
*
|
|
8
|
-
* Nothing outside this folder knows whether the site is ranked by `ts_rank`,
|
|
9
|
-
* by an InnoDB fulltext score or by BM25 — or, on a SQLite build without FTS5,
|
|
10
|
-
* not ranked at all. What every caller does know is that the locale and the
|
|
11
|
-
* state it asks for are the only ones it can be given.
|
|
12
|
-
*/
|
|
13
|
-
export { extractBlockText, extractRichText, searchDocumentFor } from './extract.js';
|
|
5
|
+
export { buildExcerpt, extractBlockText, extractRichText, searchDocumentFor, titleOf, } from './extract.js';
|
|
14
6
|
export { createMysqlSearch } from './mysql.js';
|
|
15
7
|
export { configurationFor, createPostgresSearch } from './postgres.js';
|
|
16
8
|
export { createSqliteSearch } from './sqlite.js';
|
package/dist/search/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,eAAe,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,eAAe,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAahD,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,OAAO,GACR,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAgBvF;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAA2B;IACjE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAA;IAEtB,IAAI,EAAE,CAAC,OAAO,KAAK,UAAU;QAAE,OAAO,oBAAoB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAClE,IAAI,EAAE,CAAC,OAAO,KAAK,OAAO;QAAE,OAAO,iBAAiB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAC5D,IAAI,EAAE,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,kBAAkB,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IAC7F,CAAC;IAED,MAAM,IAAI,YAAY,CAAC;QACrB,IAAI,EAAE,wBAAwB;QAC9B,OAAO,EAAE,+CAA+C,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY;QACtF,IAAI,EAAE,gEAAgE;QACtE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE;KACjC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import { type AdminThemeOverrides, type AdminThemeTemplateId, DEFAULT_ADMIN_THEME_TEMPLATE_ID } from './admin-theme-templates.js';
|
|
3
|
+
export interface AdminThemeRecord {
|
|
4
|
+
readonly templateId: AdminThemeTemplateId;
|
|
5
|
+
readonly overrides: AdminThemeOverrides;
|
|
6
|
+
readonly updatedAt: string;
|
|
7
|
+
readonly updatedBy: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface AdminThemeStoreOptions {
|
|
10
|
+
readonly db: DatabaseHandle;
|
|
11
|
+
readonly now?: () => Date;
|
|
12
|
+
}
|
|
13
|
+
export interface AdminThemeStore {
|
|
14
|
+
/** `null` when nobody has ever written a choice — the caller applies `DEFAULT_ADMIN_THEME_TEMPLATE_ID` with no overrides. */
|
|
15
|
+
get(): Promise<AdminThemeRecord | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Refuses a `templateId` outside `ADMIN_THEME_TEMPLATES`
|
|
18
|
+
* (`ADMIN_THEME_TEMPLATE_UNKNOWN`) or an override object that fails
|
|
19
|
+
* `adminThemeOverridesSchema` (`ADMIN_THEME_INVALID`) — never a loose,
|
|
20
|
+
* unvalidated blob written straight through.
|
|
21
|
+
*/
|
|
22
|
+
set(templateId: string, overrides: unknown, updatedBy: string | null): Promise<AdminThemeRecord>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createAdminThemeStore(options: AdminThemeStoreOptions): AdminThemeStore;
|
|
25
|
+
export { DEFAULT_ADMIN_THEME_TEMPLATE_ID };
|
|
26
|
+
//# sourceMappingURL=admin-theme-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-theme-store.d.ts","sourceRoot":"","sources":["../../src/store/admin-theme-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAmB,MAAM,eAAe,CAAA;AAElF,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAGzB,+BAA+B,EAChC,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAA;IACzC,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAA;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,6HAA6H;IAC7H,GAAG,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IACvC;;;;;OAKG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;CACjG;AA8BD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAqEtF;AAED,OAAO,EAAE,+BAA+B,EAAE,CAAA"}
|