@cequrebackends/plugin-surrealdb 0.14.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/README.md +21 -0
- package/dist/adapter.d.ts +69 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +533 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @cequrebackends/plugin-surrealdb
|
|
2
|
+
|
|
3
|
+
SurrealDB database driver plugin for Cequre backends.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @cequrebackends/plugin-surrealdb surrealdb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { CequreRuntime } from "@cequrebackends/cequre-ts";
|
|
15
|
+
import { surrealdb } from "@cequrebackends/plugin-surrealdb";
|
|
16
|
+
|
|
17
|
+
const runtime = new CequreRuntime(schema, {
|
|
18
|
+
adapter: surrealdb({ url: "http://127.0.0.1:8000" }),
|
|
19
|
+
plugins: []
|
|
20
|
+
});
|
|
21
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CequreAdapter, QueryArgs, PaginatedResult, CollectionConfig } from "@cequrebackends/cequre-ts";
|
|
2
|
+
export type SurrealDBOptions = {
|
|
3
|
+
url: string;
|
|
4
|
+
namespace?: string;
|
|
5
|
+
database?: string;
|
|
6
|
+
auth?: {
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* SurrealDB adapter using the official SurrealDB JavaScript SDK v3.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SurrealDBAdapter implements CequreAdapter {
|
|
15
|
+
readonly adapterType = "surrealdb";
|
|
16
|
+
readonly supportsGeneratedConstraintSQL = false;
|
|
17
|
+
private _connection;
|
|
18
|
+
private _RecordId;
|
|
19
|
+
private get db();
|
|
20
|
+
private options;
|
|
21
|
+
/** slug → ordered list of field names known from the Cequre config */
|
|
22
|
+
private _collectionFields;
|
|
23
|
+
constructor(options: SurrealDBOptions);
|
|
24
|
+
private _getRecordId;
|
|
25
|
+
/**
|
|
26
|
+
* Register collection field names so that `normalizeRecord` can hydrate
|
|
27
|
+
* fields that SurrealDB omits when their value is NONE.
|
|
28
|
+
*/
|
|
29
|
+
configureCollections(collections: CollectionConfig[]): void;
|
|
30
|
+
connect(): Promise<void>;
|
|
31
|
+
private _connectInner;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
ping(): Promise<void>;
|
|
34
|
+
getSystemTableStatements(): string[];
|
|
35
|
+
getCurrentSchema(): Promise<{
|
|
36
|
+
tables: string[];
|
|
37
|
+
columns: Record<string, string[]>;
|
|
38
|
+
}>;
|
|
39
|
+
recordMigration(version: string, description: string): Promise<void>;
|
|
40
|
+
getCurrentMigrationVersion(): Promise<string | null>;
|
|
41
|
+
createTableDDL(table: unknown, tableName?: string): string;
|
|
42
|
+
renameColumnDDL(table: string, oldName: string, _newName: string, column: unknown): string;
|
|
43
|
+
addColumnDDL(table: string, column: unknown): string | null;
|
|
44
|
+
dropColumnDDL(table: string, column: string): string;
|
|
45
|
+
dropTableDDL(table: string): string;
|
|
46
|
+
parseConstraintError(error: unknown): any;
|
|
47
|
+
find(collection: string, query: QueryArgs): Promise<PaginatedResult>;
|
|
48
|
+
count(collection: string, query?: Pick<QueryArgs, "where">): Promise<number>;
|
|
49
|
+
findById(collection: string, id: string): Promise<Record<string, unknown> | null>;
|
|
50
|
+
findByIds(collection: string, ids: string[]): Promise<Record<string, unknown>[]>;
|
|
51
|
+
create(collection: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
52
|
+
createMany(collection: string, docs: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;
|
|
53
|
+
update(collection: string, id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
54
|
+
delete(collection: string, id: string): Promise<void>;
|
|
55
|
+
deleteMany(collection: string, ids: string[]): Promise<void>;
|
|
56
|
+
raw(query: string, params?: unknown[]): Promise<unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Normalizes a SurrealDB record to a plain object with a string `id` field.
|
|
59
|
+
* SurrealDB returns `id` as a RecordId object (table:uuid) — we extract just the ID part.
|
|
60
|
+
*/
|
|
61
|
+
private normalizeRecord;
|
|
62
|
+
/**
|
|
63
|
+
* Recursively builds a SurrealQL WHERE expression supporting flat field
|
|
64
|
+
* conditions plus compound `or` / `and` arrays.
|
|
65
|
+
*/
|
|
66
|
+
private buildSurrealWhere;
|
|
67
|
+
private buildSurrealOp;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAEpE,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAErE;AAED,cAAc,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// src/adapter.ts
|
|
5
|
+
import { ulid } from "@cequrebackends/cequre-ts";
|
|
6
|
+
import { createLogger } from "@cequrebackends/cequre-ts";
|
|
7
|
+
var logger = createLogger("SurrealDBAdapter");
|
|
8
|
+
function generateSystemTablesSurreal() {
|
|
9
|
+
return ["DEFINE TABLE _cequre_migrations SCHEMALESS;", "DEFINE TABLE cequre_refresh_tokens SCHEMALESS;"];
|
|
10
|
+
}
|
|
11
|
+
function generateAddColumnSurreal(table, column) {
|
|
12
|
+
let ddl = `DEFINE FIELD ${surrealIdentifier(column.name)} ON ${surrealIdentifier(table)} TYPE any;`;
|
|
13
|
+
if (column.unique) {
|
|
14
|
+
ddl += `
|
|
15
|
+
DEFINE INDEX unique_${column.name} ON TABLE ${surrealIdentifier(table)} COLUMNS ${surrealIdentifier(column.name)} UNIQUE;`;
|
|
16
|
+
}
|
|
17
|
+
return ddl;
|
|
18
|
+
}
|
|
19
|
+
function generateRenameColumnSurreal(table, oldName, newName) {
|
|
20
|
+
return `UPDATE ${table} SET ${newName} = ${oldName}; REMOVE FIELD ${oldName} ON TABLE ${table};`;
|
|
21
|
+
}
|
|
22
|
+
var _surrealModule = null;
|
|
23
|
+
async function loadSurrealDB() {
|
|
24
|
+
if (_surrealModule)
|
|
25
|
+
return _surrealModule;
|
|
26
|
+
try {
|
|
27
|
+
_surrealModule = await import("surrealdb");
|
|
28
|
+
return _surrealModule;
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error("surrealdb is not installed. Run: bun add surrealdb");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var SURREAL_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
34
|
+
function surrealIdentifier(identifier) {
|
|
35
|
+
if (!SURREAL_IDENTIFIER_RE.test(identifier)) {
|
|
36
|
+
throw new Error(`Invalid SurrealDB identifier '${identifier}'`);
|
|
37
|
+
}
|
|
38
|
+
return identifier;
|
|
39
|
+
}
|
|
40
|
+
function surrealPath(path) {
|
|
41
|
+
return path.split(".").map(surrealIdentifier).join(".");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class SurrealDBAdapter {
|
|
45
|
+
adapterType = "surrealdb";
|
|
46
|
+
supportsGeneratedConstraintSQL = false;
|
|
47
|
+
_connection = null;
|
|
48
|
+
_RecordId = null;
|
|
49
|
+
get db() {
|
|
50
|
+
if (!this._connection)
|
|
51
|
+
throw new Error("SurrealDB not connected. Call connect() first.");
|
|
52
|
+
return this._connection;
|
|
53
|
+
}
|
|
54
|
+
options;
|
|
55
|
+
_collectionFields = new Map;
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this.options = options;
|
|
58
|
+
}
|
|
59
|
+
async _getRecordId() {
|
|
60
|
+
if (this._RecordId)
|
|
61
|
+
return this._RecordId;
|
|
62
|
+
const sdk = await loadSurrealDB();
|
|
63
|
+
this._RecordId = sdk.RecordId;
|
|
64
|
+
return this._RecordId;
|
|
65
|
+
}
|
|
66
|
+
configureCollections(collections) {
|
|
67
|
+
for (const colDef of collections) {
|
|
68
|
+
const col = colDef;
|
|
69
|
+
const names = col.fields.map((f) => f.name);
|
|
70
|
+
if (col.timestamps)
|
|
71
|
+
names.push("createdAt", "updatedAt");
|
|
72
|
+
if (col.softDelete)
|
|
73
|
+
names.push("deletedAt");
|
|
74
|
+
if (col.auth && col.requireEmailVerification)
|
|
75
|
+
names.push("emailVerified", "verifyToken");
|
|
76
|
+
if (col.auth && col.lockout)
|
|
77
|
+
names.push("loginAttempts", "lockedUntil");
|
|
78
|
+
this._collectionFields.set(col.slug, names);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async connect() {
|
|
82
|
+
const sdk = await loadSurrealDB();
|
|
83
|
+
if (!this._connection) {
|
|
84
|
+
this._connection = new sdk.Surreal;
|
|
85
|
+
}
|
|
86
|
+
this._RecordId = sdk.RecordId;
|
|
87
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`SurrealDB connection timed out after 5s \u2014 is the server running at ${this.options.url}?`)), 5000));
|
|
88
|
+
await Promise.race([this._connectInner(), timeout]);
|
|
89
|
+
}
|
|
90
|
+
async _connectInner() {
|
|
91
|
+
await this.db.connect(this.options.url);
|
|
92
|
+
if (this.options.auth) {
|
|
93
|
+
await this.db.signin({
|
|
94
|
+
username: this.options.auth.username,
|
|
95
|
+
password: this.options.auth.password
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const ns = this.options.namespace ?? "cequre-bun";
|
|
99
|
+
const db = this.options.database ?? "cequre-bun";
|
|
100
|
+
try {
|
|
101
|
+
await this.db.query(`DEFINE NAMESPACE IF NOT EXISTS ${ns};`);
|
|
102
|
+
await this.db.query(`USE NAMESPACE ${ns}; DEFINE DATABASE IF NOT EXISTS ${db};`);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
logger.warn({ err: e }, "Failed to auto-create namespace/database. It might already exist or permissions are lacking.");
|
|
105
|
+
}
|
|
106
|
+
await this.db.use({
|
|
107
|
+
namespace: ns,
|
|
108
|
+
database: db
|
|
109
|
+
});
|
|
110
|
+
await this.db.query("RETURN true");
|
|
111
|
+
logger.info("Connected to SurrealDB");
|
|
112
|
+
}
|
|
113
|
+
async disconnect() {
|
|
114
|
+
if (this._connection)
|
|
115
|
+
await this._connection.close();
|
|
116
|
+
this._connection = null;
|
|
117
|
+
}
|
|
118
|
+
async ping() {
|
|
119
|
+
await this.db.query("RETURN true");
|
|
120
|
+
}
|
|
121
|
+
getSystemTableStatements() {
|
|
122
|
+
return generateSystemTablesSurreal();
|
|
123
|
+
}
|
|
124
|
+
async getCurrentSchema() {
|
|
125
|
+
const schema = {
|
|
126
|
+
tables: [],
|
|
127
|
+
columns: {}
|
|
128
|
+
};
|
|
129
|
+
const dbInfo = await this.raw("INFO FOR DB");
|
|
130
|
+
const tablesMap = dbInfo.tables ?? {};
|
|
131
|
+
schema.tables = Object.keys(tablesMap).filter((name) => !name.startsWith("cequre-bun_") && name !== "undefined" && name !== "null");
|
|
132
|
+
for (const tableName of schema.tables) {
|
|
133
|
+
const tableInfo = await this.raw(`INFO FOR TABLE ${tableName}`);
|
|
134
|
+
const fieldsMap = tableInfo.fields ?? {};
|
|
135
|
+
schema.columns[tableName] = Object.keys(fieldsMap).map((field) => `${field} string`);
|
|
136
|
+
}
|
|
137
|
+
return schema;
|
|
138
|
+
}
|
|
139
|
+
async recordMigration(version, description) {
|
|
140
|
+
const existing = await this.find("_cequre_migrations", {
|
|
141
|
+
where: { version: { eq: version } },
|
|
142
|
+
limit: 1,
|
|
143
|
+
page: 1
|
|
144
|
+
});
|
|
145
|
+
if (existing.docs.length > 0)
|
|
146
|
+
return;
|
|
147
|
+
await this.create("_cequre_migrations", {
|
|
148
|
+
version,
|
|
149
|
+
description,
|
|
150
|
+
applied_at: new Date().toISOString()
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async getCurrentMigrationVersion() {
|
|
154
|
+
const result = await this.find("_cequre_migrations", {
|
|
155
|
+
sort: "-applied_at",
|
|
156
|
+
limit: 1,
|
|
157
|
+
page: 1
|
|
158
|
+
});
|
|
159
|
+
return result.docs[0]?.version ?? null;
|
|
160
|
+
}
|
|
161
|
+
createTableDDL(table, tableName) {
|
|
162
|
+
const name = tableName || table.name || table.slug;
|
|
163
|
+
let ddl = `DEFINE TABLE IF NOT EXISTS ${surrealIdentifier(name)} SCHEMALESS;`;
|
|
164
|
+
const fields = table.fields || [];
|
|
165
|
+
for (const field of fields) {
|
|
166
|
+
if (field.unique) {
|
|
167
|
+
ddl += `
|
|
168
|
+
DEFINE INDEX IF NOT EXISTS unique_${field.name} ON TABLE ${surrealIdentifier(name)} COLUMNS ${surrealIdentifier(field.name)} UNIQUE;`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return ddl;
|
|
172
|
+
}
|
|
173
|
+
renameColumnDDL(table, oldName, _newName, column) {
|
|
174
|
+
return generateRenameColumnSurreal(table, oldName, _newName);
|
|
175
|
+
}
|
|
176
|
+
addColumnDDL(table, column) {
|
|
177
|
+
return generateAddColumnSurreal(table, column);
|
|
178
|
+
}
|
|
179
|
+
dropColumnDDL(table, column) {
|
|
180
|
+
return `REMOVE FIELD ${surrealPath(column)} ON TABLE ${surrealIdentifier(table)};`;
|
|
181
|
+
}
|
|
182
|
+
dropTableDDL(table) {
|
|
183
|
+
return `REMOVE TABLE ${surrealIdentifier(table)};`;
|
|
184
|
+
}
|
|
185
|
+
parseConstraintError(error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
const rawMessage = message;
|
|
188
|
+
const existsMatch = message.match(/Database record `([^`]+)` already exists/i) || message.match(/already exists/i) || message.match(/already contains/i);
|
|
189
|
+
if (existsMatch) {
|
|
190
|
+
return {
|
|
191
|
+
type: "unique",
|
|
192
|
+
rawMessage
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
async find(collection, query) {
|
|
198
|
+
const { where, sort, limit = 10, page = 1, cursor } = query;
|
|
199
|
+
let whereSql = "";
|
|
200
|
+
const params = {};
|
|
201
|
+
if (where) {
|
|
202
|
+
const ctx = { params, pIdx: 0 };
|
|
203
|
+
const sql = this.buildSurrealWhere(where, ctx);
|
|
204
|
+
if (sql)
|
|
205
|
+
whereSql = " WHERE " + sql;
|
|
206
|
+
}
|
|
207
|
+
let orderSql = "";
|
|
208
|
+
if (sort) {
|
|
209
|
+
const parts = sort.split(",").map((s) => s.trim()).filter(Boolean);
|
|
210
|
+
const orderParts = parts.map((part) => {
|
|
211
|
+
const desc = part.startsWith("-");
|
|
212
|
+
const field = desc ? part.slice(1) : part;
|
|
213
|
+
return `${surrealPath(field)} ${desc ? "DESC" : "ASC"}`;
|
|
214
|
+
});
|
|
215
|
+
if (orderParts.length > 0)
|
|
216
|
+
orderSql = ` ORDER BY ${orderParts.join(", ")}`;
|
|
217
|
+
}
|
|
218
|
+
if (cursor) {
|
|
219
|
+
let decoded;
|
|
220
|
+
try {
|
|
221
|
+
decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf-8"));
|
|
222
|
+
} catch {
|
|
223
|
+
throw new Error("Invalid cursor");
|
|
224
|
+
}
|
|
225
|
+
const primarySort = sort ? sort.split(",")[0].trim() : "id";
|
|
226
|
+
const desc = primarySort.startsWith("-");
|
|
227
|
+
const sortField = desc ? primarySort.slice(1) : primarySort;
|
|
228
|
+
const op = desc ? "<" : ">";
|
|
229
|
+
const cursorCondition = sortField === "id" ? `meta::id(id) ${op} $__cursorId` : `(${surrealPath(sortField)} ${op} $__cursorSortValue OR (${surrealPath(sortField)} = $__cursorSortValue AND meta::id(id) ${op} $__cursorId))`;
|
|
230
|
+
params.__cursorId = decoded.id;
|
|
231
|
+
if (sortField !== "id")
|
|
232
|
+
params.__cursorSortValue = decoded.sortValue;
|
|
233
|
+
whereSql = whereSql ? `${whereSql} AND ${cursorCondition}` : ` WHERE ${cursorCondition}`;
|
|
234
|
+
params.__limit = limit + 1;
|
|
235
|
+
const dataSql2 = `SELECT * FROM ${surrealIdentifier(collection)}${whereSql}${orderSql} LIMIT $__limit`;
|
|
236
|
+
logger.debug({ dataSql: dataSql2, params }, "Executing SurrealDB cursor find query");
|
|
237
|
+
const [docs2] = await this.db.query(dataSql2, params);
|
|
238
|
+
const normalizedDocs2 = (docs2 ?? []).map((doc) => this.normalizeRecord(collection, doc));
|
|
239
|
+
const hasNextPage = normalizedDocs2.length > limit;
|
|
240
|
+
const resultDocs = hasNextPage ? normalizedDocs2.slice(0, limit) : normalizedDocs2;
|
|
241
|
+
let nextCursor = null;
|
|
242
|
+
if (hasNextPage && resultDocs.length > 0) {
|
|
243
|
+
const lastDoc = resultDocs[resultDocs.length - 1];
|
|
244
|
+
nextCursor = Buffer.from(JSON.stringify({ id: lastDoc?.id, sortValue: sortField !== "id" ? lastDoc[sortField] : undefined })).toString("base64url");
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
docs: resultDocs,
|
|
248
|
+
totalDocs: 0,
|
|
249
|
+
limit,
|
|
250
|
+
page: 0,
|
|
251
|
+
totalPages: 0,
|
|
252
|
+
hasNextPage,
|
|
253
|
+
hasPrevPage: true,
|
|
254
|
+
nextCursor,
|
|
255
|
+
prevCursor: null
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const offset = (page - 1) * limit;
|
|
259
|
+
const dataSql = `SELECT * FROM ${surrealIdentifier(collection)}${whereSql}${orderSql} LIMIT $__limit START $__offset`;
|
|
260
|
+
const countSql = `SELECT count() AS total FROM ${surrealIdentifier(collection)}${whereSql} GROUP ALL`;
|
|
261
|
+
params.__limit = limit;
|
|
262
|
+
params.__offset = offset;
|
|
263
|
+
logger.debug({ dataSql, countSql, params }, "Executing SurrealDB find query");
|
|
264
|
+
const [countResult] = await this.db.query(countSql, params);
|
|
265
|
+
const totalDocs = countResult?.[0]?.total ?? 0;
|
|
266
|
+
const [docs] = await this.db.query(dataSql, params);
|
|
267
|
+
const normalizedDocs = (docs ?? []).map((doc) => this.normalizeRecord(collection, doc));
|
|
268
|
+
const totalPages = Math.ceil(totalDocs / limit);
|
|
269
|
+
return {
|
|
270
|
+
docs: normalizedDocs,
|
|
271
|
+
totalDocs,
|
|
272
|
+
limit,
|
|
273
|
+
page,
|
|
274
|
+
totalPages,
|
|
275
|
+
hasNextPage: page < totalPages,
|
|
276
|
+
hasPrevPage: page > 1
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async count(collection, query) {
|
|
280
|
+
let whereSql = "";
|
|
281
|
+
const params = {};
|
|
282
|
+
if (query?.where) {
|
|
283
|
+
const ctx = { params, pIdx: 0 };
|
|
284
|
+
const sql = this.buildSurrealWhere(query.where, ctx);
|
|
285
|
+
if (sql)
|
|
286
|
+
whereSql = " WHERE " + sql;
|
|
287
|
+
}
|
|
288
|
+
const countSql = `SELECT count() AS total FROM ${surrealIdentifier(collection)}${whereSql} GROUP ALL`;
|
|
289
|
+
logger.debug({ countSql, params }, "Executing SurrealDB count query");
|
|
290
|
+
const [result] = await this.db.query(countSql, params);
|
|
291
|
+
return result?.[0]?.total ?? 0;
|
|
292
|
+
}
|
|
293
|
+
async findById(collection, id) {
|
|
294
|
+
logger.debug({ collection, id }, "Executing SurrealDB findById query");
|
|
295
|
+
const recordId = new (await this._getRecordId())(collection, id);
|
|
296
|
+
const result = await this.db.select(recordId);
|
|
297
|
+
if (!result)
|
|
298
|
+
return null;
|
|
299
|
+
return this.normalizeRecord(collection, result);
|
|
300
|
+
}
|
|
301
|
+
async findByIds(collection, ids) {
|
|
302
|
+
if (ids.length === 0)
|
|
303
|
+
return [];
|
|
304
|
+
logger.debug({ collection, count: ids.length }, "Executing SurrealDB findByIds query");
|
|
305
|
+
const RecordId = await this._getRecordId();
|
|
306
|
+
const recordIds = ids.map((id) => new RecordId(collection, id));
|
|
307
|
+
const [results] = await this.db.query(`SELECT * FROM $ids`, { ids: recordIds });
|
|
308
|
+
return (results ?? []).map((doc) => this.normalizeRecord(collection, doc));
|
|
309
|
+
}
|
|
310
|
+
async create(collection, data) {
|
|
311
|
+
const now = new Date;
|
|
312
|
+
const knownFields = new Set(this._collectionFields.get(collection) ?? []);
|
|
313
|
+
const { id: providedId, ...rest } = data;
|
|
314
|
+
const id = providedId ?? ulid();
|
|
315
|
+
const doc = { ...rest };
|
|
316
|
+
if (knownFields.size === 0 || knownFields.has("createdAt"))
|
|
317
|
+
doc.createdAt = doc.createdAt ?? now;
|
|
318
|
+
if (knownFields.size === 0 || knownFields.has("updatedAt"))
|
|
319
|
+
doc.updatedAt = now;
|
|
320
|
+
logger.debug({ collection, id, fields: Object.keys(doc) }, "Executing SurrealDB create");
|
|
321
|
+
const recordId = new (await this._getRecordId())(collection, id);
|
|
322
|
+
const result = await this.db.create(recordId).content(doc);
|
|
323
|
+
return this.normalizeRecord(collection, result);
|
|
324
|
+
}
|
|
325
|
+
async createMany(collection, docs) {
|
|
326
|
+
if (docs.length === 0)
|
|
327
|
+
return [];
|
|
328
|
+
const now = new Date;
|
|
329
|
+
const results = [];
|
|
330
|
+
const knownFields = new Set(this._collectionFields.get(collection) ?? []);
|
|
331
|
+
for (const data of docs) {
|
|
332
|
+
const { id: providedId, ...rest } = data;
|
|
333
|
+
const id = providedId ?? ulid();
|
|
334
|
+
const doc = { ...rest };
|
|
335
|
+
if (knownFields.size === 0 || knownFields.has("createdAt"))
|
|
336
|
+
doc.createdAt = doc.createdAt ?? now;
|
|
337
|
+
if (knownFields.size === 0 || knownFields.has("updatedAt"))
|
|
338
|
+
doc.updatedAt = now;
|
|
339
|
+
const recordId = new (await this._getRecordId())(collection, id);
|
|
340
|
+
const result = await this.db.create(recordId).content(doc);
|
|
341
|
+
results.push(this.normalizeRecord(collection, result));
|
|
342
|
+
}
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
async update(collection, id, data) {
|
|
346
|
+
const now = new Date;
|
|
347
|
+
const knownFields = new Set(this._collectionFields.get(collection) ?? []);
|
|
348
|
+
const updateData = { ...data };
|
|
349
|
+
if (knownFields.size === 0 || knownFields.has("updatedAt"))
|
|
350
|
+
updateData.updatedAt = now;
|
|
351
|
+
logger.debug({ collection, id, fields: Object.keys(updateData) }, "Executing SurrealDB update");
|
|
352
|
+
const recordId = new (await this._getRecordId())(collection, id);
|
|
353
|
+
const result = await this.db.update(recordId).merge(updateData);
|
|
354
|
+
if (!result) {
|
|
355
|
+
throw new Error(`Document with id '${id}' not found in collection '${collection}'`);
|
|
356
|
+
}
|
|
357
|
+
return this.normalizeRecord(collection, result);
|
|
358
|
+
}
|
|
359
|
+
async delete(collection, id) {
|
|
360
|
+
logger.debug({ collection, id }, "Executing SurrealDB delete");
|
|
361
|
+
const recordId = new (await this._getRecordId())(collection, id);
|
|
362
|
+
const result = await this.db.delete(recordId);
|
|
363
|
+
if (!result) {
|
|
364
|
+
throw new Error(`Document with id '${id}' not found in collection '${collection}'`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async deleteMany(collection, ids) {
|
|
368
|
+
if (ids.length === 0)
|
|
369
|
+
return;
|
|
370
|
+
logger.debug({ collection, count: ids.length }, "Executing SurrealDB bulk delete");
|
|
371
|
+
const RecordId = await this._getRecordId();
|
|
372
|
+
const recordIds = ids.map((id) => new RecordId(collection, id));
|
|
373
|
+
await this.db.query(`DELETE $ids`, { ids: recordIds });
|
|
374
|
+
}
|
|
375
|
+
async raw(query, params) {
|
|
376
|
+
let surrealQuery = query;
|
|
377
|
+
const namedParams = {};
|
|
378
|
+
if (params && params.length > 0) {
|
|
379
|
+
for (let i = 0;i < params.length; i++) {
|
|
380
|
+
const positional = `$${i + 1}`;
|
|
381
|
+
const named = `__raw_${i}`;
|
|
382
|
+
surrealQuery = surrealQuery.replaceAll(positional, `$${named}`);
|
|
383
|
+
namedParams[named] = params[i];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const results = await this.db.query(surrealQuery, namedParams);
|
|
388
|
+
return results[results.length - 1];
|
|
389
|
+
} catch (e) {
|
|
390
|
+
if (e.message && (e.message.includes("already exists") || e.message.includes("AlreadyExists"))) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
throw e;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
normalizeRecord(collection, record) {
|
|
397
|
+
const normalized = {};
|
|
398
|
+
const knownFields = this._collectionFields.get(collection);
|
|
399
|
+
if (knownFields) {
|
|
400
|
+
for (const field of knownFields) {
|
|
401
|
+
normalized[field] = null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
for (const [key, value] of Object.entries(record)) {
|
|
405
|
+
if (key === "id") {
|
|
406
|
+
if (this._RecordId && value instanceof this._RecordId) {
|
|
407
|
+
normalized.id = String(value.id);
|
|
408
|
+
} else if (value !== null && typeof value === "object" && "id" in value && value.id !== undefined) {
|
|
409
|
+
normalized.id = String(value.id);
|
|
410
|
+
} else if (typeof value === "string" && value.includes(":")) {
|
|
411
|
+
normalized.id = value.split(":").slice(1).join(":");
|
|
412
|
+
} else {
|
|
413
|
+
normalized[key] = value;
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
normalized[key] = value;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return normalized;
|
|
420
|
+
}
|
|
421
|
+
buildSurrealWhere(where, ctx) {
|
|
422
|
+
const conditions = [];
|
|
423
|
+
for (const [key, value] of Object.entries(where)) {
|
|
424
|
+
if (key === "or" || key === "and" || key === "OR" || key === "AND") {
|
|
425
|
+
const sub = value;
|
|
426
|
+
if (!Array.isArray(sub) || sub.length === 0)
|
|
427
|
+
continue;
|
|
428
|
+
const parts = sub.map((clause) => this.buildSurrealWhere(clause, ctx)).filter(Boolean);
|
|
429
|
+
if (parts.length > 0) {
|
|
430
|
+
const joiner = key.toLowerCase() === "or" ? " OR " : " AND ";
|
|
431
|
+
conditions.push(`(${parts.join(joiner)})`);
|
|
432
|
+
}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
436
|
+
const ops = value;
|
|
437
|
+
const mode = ops.mode === "sensitive" ? "sensitive" : "insensitive";
|
|
438
|
+
for (const [op, opValue] of Object.entries(ops)) {
|
|
439
|
+
if (op === "mode")
|
|
440
|
+
continue;
|
|
441
|
+
const cond = this.buildSurrealOp(key, op, opValue, ctx, mode);
|
|
442
|
+
if (cond)
|
|
443
|
+
conditions.push(cond);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
const p = `p${ctx.pIdx++}`;
|
|
447
|
+
const fieldExpr = key === "id" ? "meta::id(id)" : surrealPath(key);
|
|
448
|
+
conditions.push(`${fieldExpr} = $${p}`);
|
|
449
|
+
ctx.params[p] = value;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return conditions.length > 0 ? conditions.join(" AND ") : "";
|
|
453
|
+
}
|
|
454
|
+
buildSurrealOp(field, op, value, ctx, mode = "insensitive") {
|
|
455
|
+
const p = `p${ctx.pIdx++}`;
|
|
456
|
+
const fieldExpr = field === "id" ? "meta::id(id)" : surrealPath(field);
|
|
457
|
+
switch (op) {
|
|
458
|
+
case "eq":
|
|
459
|
+
case "neq":
|
|
460
|
+
case "gt":
|
|
461
|
+
case "gte":
|
|
462
|
+
case "lt":
|
|
463
|
+
case "lte": {
|
|
464
|
+
const sym = { eq: "=", neq: "!=", gt: ">", gte: ">=", lt: "<", lte: "<=" }[op];
|
|
465
|
+
ctx.params[p] = value;
|
|
466
|
+
return `${fieldExpr} ${sym} $${p}`;
|
|
467
|
+
}
|
|
468
|
+
case "in":
|
|
469
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
470
|
+
ctx.params[p] = value;
|
|
471
|
+
return `${fieldExpr} IN $${p}`;
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
case "nin":
|
|
475
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
476
|
+
ctx.params[p] = value;
|
|
477
|
+
return `${fieldExpr} NOT IN $${p}`;
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
case "exists":
|
|
481
|
+
ctx.pIdx--;
|
|
482
|
+
return value === true || value === "true" ? `${fieldExpr} != NONE` : `(${fieldExpr} = NONE OR ${fieldExpr} IS NULL)`;
|
|
483
|
+
case "like":
|
|
484
|
+
case "contains":
|
|
485
|
+
ctx.params[p] = value;
|
|
486
|
+
return `${fieldExpr} CONTAINS $${p}`;
|
|
487
|
+
case "startsWith":
|
|
488
|
+
ctx.params[p] = value;
|
|
489
|
+
return `${fieldExpr} CONTAINS $${p}`;
|
|
490
|
+
case "endsWith":
|
|
491
|
+
ctx.params[p] = value;
|
|
492
|
+
return `${fieldExpr} CONTAINS $${p}`;
|
|
493
|
+
case "notContains":
|
|
494
|
+
ctx.params[p] = value;
|
|
495
|
+
return `${fieldExpr} CONTAINSNOT $${p}`;
|
|
496
|
+
case "any":
|
|
497
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
498
|
+
ctx.params[p] = value;
|
|
499
|
+
return `${fieldExpr} CONTAINSANY $${p}`;
|
|
500
|
+
}
|
|
501
|
+
ctx.pIdx--;
|
|
502
|
+
return null;
|
|
503
|
+
case "all":
|
|
504
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
505
|
+
ctx.params[p] = value;
|
|
506
|
+
return `${fieldExpr} CONTAINSALL $${p}`;
|
|
507
|
+
}
|
|
508
|
+
ctx.pIdx--;
|
|
509
|
+
return null;
|
|
510
|
+
case "between": {
|
|
511
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
512
|
+
const p2 = `p${ctx.pIdx++}`;
|
|
513
|
+
ctx.params[p] = value[0];
|
|
514
|
+
ctx.params[p2] = value[1];
|
|
515
|
+
return `${fieldExpr} >= $${p} AND ${fieldExpr} <= $${p2}`;
|
|
516
|
+
}
|
|
517
|
+
ctx.pIdx--;
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
default:
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/index.ts
|
|
527
|
+
function surrealdb(options) {
|
|
528
|
+
return new SurrealDBAdapter(options);
|
|
529
|
+
}
|
|
530
|
+
export {
|
|
531
|
+
surrealdb,
|
|
532
|
+
SurrealDBAdapter
|
|
533
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cequrebackends/plugin-surrealdb",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "rm -rf dist && bun build ./src/index.ts --outdir ./dist --target bun --packages external && tsc --emitDeclarationOnly"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"surrealdb": "^2.0.4"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^7.0.1-rc",
|
|
19
|
+
"@cequrebackends/cequre-ts": "0.13.0"
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/cequrebackends/cequre.git"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@cequrebackends/cequre-ts": "0.13.0"
|
|
39
|
+
}
|
|
40
|
+
}
|