@omgbase/core 0.4.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/src/core/ids.d.ts +34 -3
- package/dist/src/core/ids.js +79 -6
- package/dist/src/core/store/fts.d.ts +9 -0
- package/dist/src/core/store/fts.js +13 -0
- package/dist/src/core/store/store.d.ts +4 -0
- package/dist/src/core/store/store.js +40 -1
- package/dist/src/format/index.d.ts +12 -1
- package/dist/src/format/index.js +14 -1
- package/dist/src/format/markdown.d.ts +7 -0
- package/dist/src/format/markdown.js +7 -0
- package/dist/src/graph/history.d.ts +21 -0
- package/dist/src/graph/history.js +48 -6
- package/dist/src/graph/links.d.ts +2 -0
- package/dist/src/graph/links.js +22 -11
- package/dist/src/index.d.ts +6 -5
- package/dist/src/index.js +6 -5
- package/dist/src/mcp/errors.d.ts +8 -0
- package/dist/src/mcp/errors.js +17 -3
- package/dist/src/mcp/server.js +32 -50
- package/dist/src/mutate/apply.js +12 -4
- package/dist/src/mutate/doc-store.js +4 -1
- package/dist/src/mutate/docs.d.ts +19 -0
- package/dist/src/mutate/docs.js +64 -16
- package/dist/src/mutate/ops.js +25 -13
- package/dist/src/oqx-js/sql/translate.d.ts +36 -13
- package/dist/src/oqx-js/sql/translate.js +122 -33
- package/dist/src/search/tasks.d.ts +9 -0
- package/dist/src/search/tasks.js +23 -1
- package/dist/src/sync/admin.d.ts +13 -0
- package/dist/src/sync/admin.js +11 -0
- package/package.json +1 -1
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
// KINDS of its two operands are provably compared the same way — see
|
|
29
29
|
// `comparable` (the spec/surface §1 decline (a)) — and a name that is a relation, reach-through
|
|
30
30
|
// or source handle is never read as a property (decline (b),
|
|
31
|
-
// `NON_PROPERTY_NAMES`).
|
|
31
|
+
// `NON_PROPERTY_NAMES`). A bool or num literal against a JSON or property
|
|
32
|
+
// read is a TYPED push (the 1.2 patch): the stored type is tested in SQL
|
|
33
|
+
// before the value (`json_type(x) = 'true'`, `p.type = 'number' AND
|
|
34
|
+
// p.val_num >= ?`) and the whole test is wrapped `(…) IS 1` / `IS NOT 1`,
|
|
35
|
+
// so an absent or differently typed value compares as in memory —
|
|
36
|
+
// unequal, never ordered. See `typedComparison`.
|
|
32
37
|
//
|
|
33
38
|
// Only forms that are faithful in a POSITIVE, AND-composed context are
|
|
34
39
|
// translated (that is the only context `partitionPushable` pushes into). `||`,
|
|
@@ -103,38 +108,46 @@ function intrinsicSql(name, ctx) {
|
|
|
103
108
|
* unplanned when one is residual: spec/surface §1 decline (c)). */
|
|
104
109
|
export const RESERVED_DOC_BASENAMES = new Set(["id", "path", "updated_at", "content_hash", "body"]);
|
|
105
110
|
const SEG = /^[A-Za-z_][A-Za-z0-9_]*$/; // injection-safe inlined identifier
|
|
106
|
-
//
|
|
107
|
-
//
|
|
111
|
+
// The single-scalar-row scope of a document property (the CEL scalar-in-scope
|
|
112
|
+
// rule): the row for `key` on the document only when the key has exactly one
|
|
113
|
+
// row in scope and it is card='scalar' — a list-valued or nested key has none.
|
|
114
|
+
// `select` is the projected expression over the alias `p`.
|
|
115
|
+
function propSubquery(docAlias, key, select) {
|
|
116
|
+
return `(SELECT ${select} FROM properties p
|
|
117
|
+
WHERE p.doc_id = ${docAlias}.doc_id AND p.key = '${key}' AND p.card = 'scalar' AND p.deleted_commit IS NULL
|
|
118
|
+
AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = ${docAlias}.doc_id AND p2.key = '${key}' AND p2.deleted_commit IS NULL) = 1
|
|
119
|
+
LIMIT 1)`;
|
|
120
|
+
}
|
|
121
|
+
// A single-valued document property: the scalar value of its one row in scope,
|
|
108
122
|
// else NULL — matching the store context's `docProp` for a scalar read.
|
|
109
123
|
function propScalar(docAlias, key) {
|
|
110
124
|
if (!SEG.test(key))
|
|
111
125
|
return null;
|
|
112
126
|
return {
|
|
113
|
-
sql:
|
|
114
|
-
WHERE p.doc_id = ${docAlias}.doc_id AND p.key = '${key}' AND p.card = 'scalar' AND p.deleted_commit IS NULL
|
|
115
|
-
AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = ${docAlias}.doc_id AND p2.key = '${key}' AND p2.deleted_commit IS NULL) = 1
|
|
116
|
-
LIMIT 1)`,
|
|
127
|
+
sql: propSubquery(docAlias, key, "COALESCE(p.val_text, p.val_num, p.val_bool)"),
|
|
117
128
|
params: [],
|
|
118
129
|
kind: "prop",
|
|
130
|
+
prop: { docAlias, key },
|
|
119
131
|
};
|
|
120
132
|
}
|
|
121
133
|
// json_extract path from validated segments, or null if any segment is unsafe.
|
|
122
134
|
function jsonExtract(col, segs) {
|
|
123
135
|
if (segs.some((s) => !SEG.test(s)))
|
|
124
136
|
return null;
|
|
125
|
-
|
|
137
|
+
const path = `'$.${segs.join(".")}'`;
|
|
138
|
+
return { sql: `json_extract(${col}, ${path})`, params: [], kind: "json", json: { col, path } };
|
|
126
139
|
}
|
|
127
140
|
// A literal or binding value as a bound `?` with its kind. better-sqlite3 binds
|
|
128
141
|
// only number/string/bigint/buffer/null — booleans are coerced to 1/0 (how
|
|
129
|
-
// json_extract
|
|
130
|
-
// not a scalar and declines.
|
|
142
|
+
// json_extract and `val_bool` surface booleans); anything else (an array
|
|
143
|
+
// binding, say) is not a scalar and declines.
|
|
131
144
|
function constOperand(v) {
|
|
132
145
|
switch (typeof v) {
|
|
133
|
-
case "string": return { sql: "?", params: [v], kind: "text" };
|
|
134
|
-
case "number": return { sql: "?", params: [v], kind: "num" };
|
|
135
|
-
case "boolean": return { sql: "?", params: [v ? 1 : 0], kind: "bool" };
|
|
136
|
-
case "undefined": return { sql: "?", params: [null], kind: "null" };
|
|
137
|
-
default: return v === null ? { sql: "?", params: [null], kind: "null" } : null;
|
|
146
|
+
case "string": return { sql: "?", params: [v], kind: "text", value: v };
|
|
147
|
+
case "number": return { sql: "?", params: [v], kind: "num", value: v };
|
|
148
|
+
case "boolean": return { sql: "?", params: [v ? 1 : 0], kind: "bool", value: v };
|
|
149
|
+
case "undefined": return { sql: "?", params: [null], kind: "null", value: null };
|
|
150
|
+
default: return v === null ? { sql: "?", params: [null], kind: "null", value: null } : null;
|
|
138
151
|
}
|
|
139
152
|
}
|
|
140
153
|
// The dotted `attrs.a.b` / `doc.x` receiver chain as segments, or null if it is
|
|
@@ -262,35 +275,49 @@ const IS_OP = {
|
|
|
262
275
|
* comparison is pushed only when its two operand kinds are provably compared
|
|
263
276
|
* the same way by SQLite and by the in-memory `equals`/`compare`.
|
|
264
277
|
*
|
|
265
|
-
* Equality (`==`, `!=`) pushes iff
|
|
278
|
+
* Equality (`==`, `!=`) pushes PLAINLY (`IS` / `IS NOT`) iff
|
|
266
279
|
* • one operand is text — SQLite's typed `IS` and strict `===` agree that a
|
|
267
280
|
* string equals nothing but an equal string (`5 IS '5'` is false);
|
|
268
281
|
* • or one operand is null and the other is NOT a property read — the same
|
|
269
282
|
* absence both ways, except that a list-valued or nested property has no
|
|
270
283
|
* scalar row (SQL NULL) where in memory the value is an array or object;
|
|
271
284
|
* • or both operands are numeric (int or num) — an exact integer comparison.
|
|
272
|
-
* Relational (`<`, `<=`, `>`, `>=`) pushes iff both operands are text
|
|
273
|
-
* are numeric.
|
|
285
|
+
* Relational (`<`, `<=`, `>`, `>=`) pushes plainly iff both operands are text
|
|
286
|
+
* or both are numeric.
|
|
287
|
+
* TYPED pushes (the 1.2 patch, `typedComparison`): a bool literal/binding
|
|
288
|
+
* against a json or prop read (`==`, `!=` only) and a num literal/binding
|
|
289
|
+
* against a json or prop read (every op) push with the stored type tested
|
|
290
|
+
* first — `json_type(x) = 'true'`, `json_type(x) IN ('integer','real') AND
|
|
291
|
+
* json_extract(x) <op> ?`, `p.type = 'bool' AND p.val_bool = ?`, `p.type =
|
|
292
|
+
* 'number' AND p.val_num <op> ?` — wrapped `(…) IS 1` (or `IS NOT 1` for
|
|
293
|
+
* `!=`), so an absent or differently typed value is unequal and never
|
|
294
|
+
* ordered, exactly as in memory.
|
|
274
295
|
* Every other pair declines: SQLite sees JSON `true` and `1`, `val_bool` and
|
|
275
|
-
* `val_num` alike (
|
|
276
|
-
*
|
|
277
|
-
*
|
|
296
|
+
* `val_num` alike (`$ordinal == checked`, `true == 1`), orders every
|
|
297
|
+
* integer before every text (`$ordinal < "3"`, `level < "x"`, `$path > 5`),
|
|
298
|
+
* and two JSON or property reads carry no type at plan time
|
|
278
299
|
* (`attrs.a == attrs.b`, `era == stages`).
|
|
279
300
|
*
|
|
280
|
-
* The resulting grid (P = pushed,
|
|
301
|
+
* The resulting grid (P = pushed plainly, T = typed push, D = declined;
|
|
302
|
+
* symmetric) for `==` / `!=`:
|
|
281
303
|
*
|
|
282
304
|
* text int num bool null json prop
|
|
283
305
|
* text P P P P P P P
|
|
284
306
|
* int P P P D P D D
|
|
285
|
-
* num P P P D P
|
|
286
|
-
* bool P D D D P
|
|
307
|
+
* num P P P D P T T
|
|
308
|
+
* bool P D D D P T T
|
|
287
309
|
* null P P P P P P D
|
|
288
|
-
* json P D
|
|
289
|
-
* prop P D
|
|
310
|
+
* json P D T T P D D
|
|
311
|
+
* prop P D T T D D D
|
|
290
312
|
*
|
|
291
|
-
* and for `<` `<=` `>` `>=`
|
|
313
|
+
* and for `<` `<=` `>` `>=` text×text, int×int, int×num, num×num push plainly
|
|
314
|
+
* and num×json, num×prop push typed.
|
|
292
315
|
*/
|
|
293
316
|
export function comparable(op, a, b) {
|
|
317
|
+
return plainComparable(op, a, b) || typedPair(op, a, b) !== null;
|
|
318
|
+
}
|
|
319
|
+
// The plain cells of the grid (P): the two operands compare the same way as-is.
|
|
320
|
+
function plainComparable(op, a, b) {
|
|
294
321
|
const numeric = (k) => k === "int" || k === "num";
|
|
295
322
|
const bothNumeric = numeric(a) && numeric(b);
|
|
296
323
|
if (op === "==" || op === "!=") {
|
|
@@ -302,6 +329,18 @@ export function comparable(op, a, b) {
|
|
|
302
329
|
}
|
|
303
330
|
return (a === "text" && b === "text") || bothNumeric;
|
|
304
331
|
}
|
|
332
|
+
// The typed cells of the grid (T): which side is the constant (`"left"` /
|
|
333
|
+
// `"right"`), or null when the pair is not a typed push. A bool constant pushes
|
|
334
|
+
// typed only under `==` / `!=` (booleans never order); a num constant under any op.
|
|
335
|
+
function typedPair(op, a, b) {
|
|
336
|
+
const read = (k) => k === "json" || k === "prop";
|
|
337
|
+
const constant = (k) => k === "num" || (k === "bool" && (op === "==" || op === "!="));
|
|
338
|
+
if (constant(a) && read(b))
|
|
339
|
+
return "left";
|
|
340
|
+
if (constant(b) && read(a))
|
|
341
|
+
return "right";
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
305
344
|
/** The complement of `comparable` — kept for readers of the earlier decline-list form. */
|
|
306
345
|
export function declinedPair(a, b, op = "==") {
|
|
307
346
|
return !comparable(op, a, b);
|
|
@@ -333,13 +372,63 @@ function translateComparison(e, ctx) {
|
|
|
333
372
|
const r = translateOperand(e.right, ctx);
|
|
334
373
|
if (!l || !r)
|
|
335
374
|
return null;
|
|
336
|
-
if (
|
|
375
|
+
if (plainComparable(e.op, l.kind, r.kind)) {
|
|
376
|
+
// `==`/`!=` → null-safe IS / IS NOT (absence-normalized equality, faithful in
|
|
377
|
+
// any context). Relational ops → plain SQL: a NULL operand yields NULL, which
|
|
378
|
+
// is excluded in the positive AND context these fragments are pushed into,
|
|
379
|
+
// matching `relate`'s absent-operand ⇒ false rule.
|
|
380
|
+
return { sql: `(${l.sql} ${op} ${r.sql})`, params: [...l.params, ...r.params] };
|
|
381
|
+
}
|
|
382
|
+
const side = typedPair(e.op, l.kind, r.kind);
|
|
383
|
+
if (side === null)
|
|
337
384
|
return null; // decline (a): the positive comparison rule
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
385
|
+
// Normalize to <read> <op> <constant>, flipping a relational op when the
|
|
386
|
+
// constant is on the left (`800 < era` ⇔ `era > 800`).
|
|
387
|
+
return side === "right" ? typedComparison(e.op, l, r) : typedComparison(FLIP[e.op] ?? e.op, r, l);
|
|
388
|
+
}
|
|
389
|
+
const FLIP = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" };
|
|
390
|
+
/**
|
|
391
|
+
* The typed push (spec/surface §1, 1.2 patch): `read <op> constant` where the
|
|
392
|
+
* read is a JSON or property read and the constant a bool or num literal/binding.
|
|
393
|
+
* The stored type is tested before the value, and the whole test is wrapped
|
|
394
|
+
* `(…) IS 1` for `==` and the relational ops, `(…) IS NOT 1` for `!=` — so a
|
|
395
|
+
* NULL (absent path, no scalar row) or a `0` (other type, unequal value) reads
|
|
396
|
+
* as "not equal" / "not ordered", which is the in-memory answer: `checked == 1`
|
|
397
|
+
* finds nothing, `checked != 1` everything, `level >= 2` only numeric levels.
|
|
398
|
+
*
|
|
399
|
+
* json × bool (json_type(col, path) = 'true'|'false') IS [NOT] 1 no bind: the type string IS the value
|
|
400
|
+
* json × num (json_type(col, path) IN ('integer','real') AND json_extract(col, path) <op> ?) IS [NOT] 1
|
|
401
|
+
* prop × bool (SELECT p.type = 'bool' AND p.val_bool = ? FROM properties p WHERE <scope> LIMIT 1) IS [NOT] 1 ? = 1|0
|
|
402
|
+
* prop × num (SELECT p.type = 'number' AND p.val_num <op> ? FROM properties p WHERE <scope> LIMIT 1) IS [NOT] 1
|
|
403
|
+
*
|
|
404
|
+
* where <op> is `=` for both `==` and `!=` (the `!=` case negates the equality
|
|
405
|
+
* by its `IS NOT 1` wrap) and the relational operator itself otherwise.
|
|
406
|
+
*
|
|
407
|
+
* `<scope>` is `propSubquery`'s single-scalar-row condition, so a list-valued or
|
|
408
|
+
* nested key yields NULL → unequal/unordered, as in memory.
|
|
409
|
+
*/
|
|
410
|
+
function typedComparison(op, read, constant) {
|
|
411
|
+
// `!=` is `!equals`: the EQUALITY test, wrapped IS NOT 1 (never `<>` inside —
|
|
412
|
+
// `(type AND val <> ?) IS NOT 1` would drop every row of the right type with a
|
|
413
|
+
// different value, the differential caught it).
|
|
414
|
+
const wrap = op === "!=" ? "IS NOT 1" : "IS 1";
|
|
415
|
+
const cmp = op === "==" || op === "!=" ? "=" : op;
|
|
416
|
+
if (read.json) {
|
|
417
|
+
const { col, path } = read.json;
|
|
418
|
+
if (constant.kind === "bool") {
|
|
419
|
+
return { sql: `((json_type(${col}, ${path}) = '${constant.value ? "true" : "false"}') ${wrap})`, params: [] };
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
sql: `((json_type(${col}, ${path}) IN ('integer', 'real') AND json_extract(${col}, ${path}) ${cmp} ?) ${wrap})`,
|
|
423
|
+
params: [...constant.params],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (read.prop) {
|
|
427
|
+
const { docAlias, key } = read.prop;
|
|
428
|
+
const test = constant.kind === "bool" ? `p.type = 'bool' AND p.val_bool = ?` : `p.type = 'number' AND p.val_num ${cmp} ?`;
|
|
429
|
+
return { sql: `(${propSubquery(docAlias, key, test)} ${wrap})`, params: [...constant.params] };
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
343
432
|
}
|
|
344
433
|
// startsWith / contains / endsWith — CASE-SENSITIVE, via substr/instr (never
|
|
345
434
|
// LIKE). `matches` (regex) is declined until a `regexp` UDF is registered.
|
|
@@ -35,6 +35,15 @@ export declare function makeBlockContextResolver(store: Store, scope: {
|
|
|
35
35
|
* `shouldEmbed` floor.
|
|
36
36
|
*/
|
|
37
37
|
export declare function buildEmbedTasks(store: Store, repoId: string): EmbedTask[];
|
|
38
|
+
/**
|
|
39
|
+
* The embedding queue depth without a provider process (spec/search §2.3): the
|
|
40
|
+
* embeddable blocks (`buildEmbedTasks`) whose current `(content_hash, ctx_hash)`
|
|
41
|
+
* has no cached vector — for `model` when one is named (the `embedding.model`
|
|
42
|
+
* setting), else for any model. `omg status` reports it as `embedQueue`;
|
|
43
|
+
* `EmbeddingWorker.staleBlocks` is the same test keyed by the connected
|
|
44
|
+
* provider's model.
|
|
45
|
+
*/
|
|
46
|
+
export declare function staleEmbedCount(store: Store, repoId: string, model?: string): number;
|
|
38
47
|
/**
|
|
39
48
|
* Build DocEmbedTask[] for every live document in the repo (doc-grain semantic
|
|
40
49
|
* retrieval). Each task carries the whole-document embed input (header +
|
package/dist/src/search/tasks.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { reconstructContent } from "../core/read/document.js";
|
|
2
2
|
import { docPropertiesMerged } from "../core/store/properties.js";
|
|
3
3
|
import { LIVE_LEAF_SQL } from "../core/store/fts.js";
|
|
4
|
-
import { contextPrefix, shouldEmbed, estimateTokens } from "./embeddings.js";
|
|
4
|
+
import { contextPrefix, ctxHashHex, shouldEmbed, estimateTokens } from "./embeddings.js";
|
|
5
5
|
function docTitle(titleByDoc, firstHeadingByDoc, docId, path) {
|
|
6
6
|
const t = titleByDoc.get(docId);
|
|
7
7
|
if (t && t.trim())
|
|
@@ -85,6 +85,28 @@ export function buildEmbedTasks(store, repoId) {
|
|
|
85
85
|
}
|
|
86
86
|
return tasks;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The embedding queue depth without a provider process (spec/search §2.3): the
|
|
90
|
+
* embeddable blocks (`buildEmbedTasks`) whose current `(content_hash, ctx_hash)`
|
|
91
|
+
* has no cached vector — for `model` when one is named (the `embedding.model`
|
|
92
|
+
* setting), else for any model. `omg status` reports it as `embedQueue`;
|
|
93
|
+
* `EmbeddingWorker.staleBlocks` is the same test keyed by the connected
|
|
94
|
+
* provider's model.
|
|
95
|
+
*/
|
|
96
|
+
export function staleEmbedCount(store, repoId, model) {
|
|
97
|
+
const tasks = buildEmbedTasks(store, repoId);
|
|
98
|
+
const cached = model
|
|
99
|
+
? store.db.prepare("SELECT 1 FROM embeddings WHERE content_hash = ? AND ctx_hash = ? AND model = ?")
|
|
100
|
+
: store.db.prepare("SELECT 1 FROM embeddings WHERE content_hash = ? AND ctx_hash = ?");
|
|
101
|
+
let stale = 0;
|
|
102
|
+
for (const t of tasks) {
|
|
103
|
+
const key = [Buffer.from(t.contentHashHex, "hex"), Buffer.from(ctxHashHex(t.ctx), "hex")];
|
|
104
|
+
const hit = model ? cached.get(...key, model) : cached.get(...key);
|
|
105
|
+
if (!hit)
|
|
106
|
+
stale++;
|
|
107
|
+
}
|
|
108
|
+
return stale;
|
|
109
|
+
}
|
|
88
110
|
// The doc-embedding header: a lightweight identifying line prepended to the
|
|
89
111
|
// body so schema-y atomic notes (a two-line canon definition) embed with their
|
|
90
112
|
// title + type/layer context rather than as near-empty bodies. Kept minimal and
|
package/dist/src/sync/admin.d.ts
CHANGED
|
@@ -57,3 +57,16 @@ export interface SyncStatus {
|
|
|
57
57
|
* re-ingested — the exact stale-under-green-light failure this guards against.
|
|
58
58
|
*/
|
|
59
59
|
export declare function syncStatus(store: Store, repoId: string, rootPath?: string): SyncStatus;
|
|
60
|
+
/** The `repos` tool's result: every repo of the workspace and whether an `fs` source feeds it. */
|
|
61
|
+
export interface ReposList {
|
|
62
|
+
repos: {
|
|
63
|
+
slug: string;
|
|
64
|
+
hasSource: boolean;
|
|
65
|
+
}[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `repos` (spec/surface §4): the workspace's repos by slug, `hasSource` when an
|
|
69
|
+
* `fs` source is attached. Shared by the MCP tool and `omg repos --json` so the
|
|
70
|
+
* shape cannot drift; per-repo counts are `reposStatus`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function reposList(store: Store): ReposList;
|
package/dist/src/sync/admin.js
CHANGED
|
@@ -50,4 +50,15 @@ export function syncStatus(store, repoId, rootPath) {
|
|
|
50
50
|
disk: status.disk,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* `repos` (spec/surface §4): the workspace's repos by slug, `hasSource` when an
|
|
55
|
+
* `fs` source is attached. Shared by the MCP tool and `omg repos --json` so the
|
|
56
|
+
* shape cannot drift; per-repo counts are `reposStatus`.
|
|
57
|
+
*/
|
|
58
|
+
export function reposList(store) {
|
|
59
|
+
const rows = store.db
|
|
60
|
+
.prepare("SELECT r.slug AS slug, MAX(CASE WHEN s.adapter = 'fs' THEN 1 ELSE 0 END) AS has_fs FROM repos r LEFT JOIN attachments a ON a.repo_id = r.repo_id LEFT JOIN sources s ON s.source_id = a.source_id GROUP BY r.repo_id, r.slug ORDER BY r.slug")
|
|
61
|
+
.all();
|
|
62
|
+
return { repos: rows.map((r) => ({ slug: r.slug, hasSource: r.has_fs === 1 })) };
|
|
63
|
+
}
|
|
53
64
|
//# sourceMappingURL=admin.js.map
|