@njinlabs/njin 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/modules/surreal.ts +48 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@njinlabs/njin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A modern framework for building company profiles, landing pages, and content-driven websites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["bun", "elysia", "surrealdb", "edgejs", "cms", "framework"],
|
package/src/modules/surreal.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { getConfig } from "../core/config";
|
|
2
3
|
import { resolveSearchPlan } from "../core/model";
|
|
3
4
|
import { makeModule } from "../core/module";
|
|
@@ -19,6 +20,13 @@ export const isRemotePath = (path: string) => REMOTE_SCHEMES.some((scheme) => pa
|
|
|
19
20
|
// match) and still tolerate minor typos, similar to trigram search.
|
|
20
21
|
const SEARCH_ANALYZER = "njin_search";
|
|
21
22
|
|
|
23
|
+
// Records the hash of the last schema this DB was migrated to, so a worker booting against
|
|
24
|
+
// an already-migrated DB (idle-evict/crash respawn — every DEFINE below is idempotent but
|
|
25
|
+
// still a full network round trip per statement) can skip straight past ensureTables(). Keyed
|
|
26
|
+
// off the DB itself rather than the process/build: a worker pointed at a fresh or different DB
|
|
27
|
+
// (e.g. a per-client env change) finds no matching record here and still runs the DEFINEs.
|
|
28
|
+
const SCHEMA_META_TABLE = "njin_schema_meta";
|
|
29
|
+
|
|
22
30
|
// SurrealDB's `GROUP ALL` aggregate (used for count queries) throws NotFoundError
|
|
23
31
|
// on a table that has never had a row created, unlike plain SELECT. Defining every
|
|
24
32
|
// table up front avoids that first-run failure (e.g. /api/setup/status before any user exists).
|
|
@@ -36,25 +44,11 @@ const ensureTables = async (db: Surreal) => {
|
|
|
36
44
|
const prefixes = new Set<string>(models.map((model) => model.prefix));
|
|
37
45
|
prefixes.add("vars");
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const defineSearchIndex = async (targetPrefix: string, field: string) => {
|
|
46
|
-
const key = `${targetPrefix}.${field}`;
|
|
47
|
-
if (definedIndexes.has(key)) return;
|
|
48
|
-
definedIndexes.add(key);
|
|
49
|
-
|
|
50
|
-
// FULLTEXT, not SEARCH — this SurrealDB version renamed the index-type keyword;
|
|
51
|
-
// SEARCH ANALYZER ... is a parse error here even though older docs/examples use it.
|
|
52
|
-
await db.query(
|
|
53
|
-
`DEFINE INDEX IF NOT EXISTS idx_search_${targetPrefix}_${field} ON TABLE ${targetPrefix} FIELDS ${field} FULLTEXT ANALYZER ${SEARCH_ANALYZER} BM25 HIGHLIGHTS;`,
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const definedIndexes = new Set<string>(); // dedupe prefix+field in case two factories share a prefix, or a nested reference targets an already-indexed field
|
|
47
|
+
// Resolve every search index's target table+field up front (also dedupes prefix+field in
|
|
48
|
+
// case two factories share a prefix, or a nested reference targets an already-indexed
|
|
49
|
+
// field) — needed both to compute the schema hash below and to drive the DEFINE INDEX loop
|
|
50
|
+
// further down.
|
|
51
|
+
const searchIndexes = new Map<string, { prefix: string; field: string }>();
|
|
58
52
|
for (const model of models) {
|
|
59
53
|
// A nested searchFields entry (e.g. "author.name") needs its index defined on the
|
|
60
54
|
// *target* table/field instead — the local relation field holds a record link, not
|
|
@@ -67,13 +61,43 @@ const ensureTables = async (db: Surreal) => {
|
|
|
67
61
|
: (model.searchFields ?? []).map((field) => ({ kind: "flat" as const, field }));
|
|
68
62
|
|
|
69
63
|
for (const entry of plan) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
64
|
+
const target =
|
|
65
|
+
entry.kind === "flat"
|
|
66
|
+
? { prefix: model.prefix, field: entry.field }
|
|
67
|
+
: { prefix: entry.targetPrefix, field: entry.targetField };
|
|
68
|
+
searchIndexes.set(`${target.prefix}.${target.field}`, target);
|
|
75
69
|
}
|
|
76
70
|
}
|
|
71
|
+
|
|
72
|
+
const schemaHash = createHash("sha256")
|
|
73
|
+
.update(JSON.stringify({ prefixes: [...prefixes].sort(), searchIndexes: [...searchIndexes.keys()].sort() }))
|
|
74
|
+
.digest("hex");
|
|
75
|
+
|
|
76
|
+
// Must be DEFINE'd before the SELECT below can even run — unlike a table that exists but
|
|
77
|
+
// has no rows (see the GROUP ALL note above), a table SurrealDB has never seen DEFINE'd at
|
|
78
|
+
// all makes ANY query against a specific record id in it throw NotFoundError, plain SELECT
|
|
79
|
+
// included. One extra round trip on every boot (skip path too), still far cheaper than the
|
|
80
|
+
// N DEFINEs it's gating.
|
|
81
|
+
await db.query(`DEFINE TABLE IF NOT EXISTS ${SCHEMA_META_TABLE} SCHEMALESS;`);
|
|
82
|
+
|
|
83
|
+
const [rows] = await db.query<[{ hash: string }[]]>(`SELECT hash FROM ${SCHEMA_META_TABLE}:current;`);
|
|
84
|
+
if (rows?.[0]?.hash === schemaHash) return;
|
|
85
|
+
|
|
86
|
+
for (const prefix of prefixes) {
|
|
87
|
+
await db.query(`DEFINE TABLE IF NOT EXISTS ${prefix} SCHEMALESS;`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await db.query(`DEFINE ANALYZER IF NOT EXISTS ${SEARCH_ANALYZER} TOKENIZERS blank FILTERS lowercase,ngram(2,10);`);
|
|
91
|
+
|
|
92
|
+
for (const { prefix: targetPrefix, field } of searchIndexes.values()) {
|
|
93
|
+
// FULLTEXT, not SEARCH — this SurrealDB version renamed the index-type keyword;
|
|
94
|
+
// SEARCH ANALYZER ... is a parse error here even though older docs/examples use it.
|
|
95
|
+
await db.query(
|
|
96
|
+
`DEFINE INDEX IF NOT EXISTS idx_search_${targetPrefix}_${field} ON TABLE ${targetPrefix} FIELDS ${field} FULLTEXT ANALYZER ${SEARCH_ANALYZER} BM25 HIGHLIGHTS;`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await db.query(`UPSERT ${SCHEMA_META_TABLE}:current SET hash = '${schemaHash}';`);
|
|
77
101
|
};
|
|
78
102
|
|
|
79
103
|
const surreal = makeModule(() => {
|