@korajs/server 1.0.0-beta.1 → 1.0.0-beta.4
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/index.cjs +454 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +543 -278
- package/dist/index.d.ts +543 -278
- package/dist/index.js +454 -193
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -176,6 +176,7 @@ __export(index_exports, {
|
|
|
176
176
|
MixedAuthProvider: () => MixedAuthProvider,
|
|
177
177
|
NoAuthProvider: () => NoAuthProvider,
|
|
178
178
|
PostgresServerStore: () => PostgresServerStore,
|
|
179
|
+
RETRIABLE_REJECTION_CODES: () => RETRIABLE_REJECTION_CODES,
|
|
179
180
|
SqliteServerStore: () => SqliteServerStore,
|
|
180
181
|
TokenAuthProvider: () => TokenAuthProvider,
|
|
181
182
|
WsServerTransport: () => WsServerTransport,
|
|
@@ -186,7 +187,8 @@ __export(index_exports, {
|
|
|
186
187
|
createPrettyLogger: () => createPrettyLogger,
|
|
187
188
|
createProductionServer: () => createProductionServer,
|
|
188
189
|
createSilentLogger: () => createSilentLogger,
|
|
189
|
-
createSqliteServerStore: () => createSqliteServerStore
|
|
190
|
+
createSqliteServerStore: () => createSqliteServerStore,
|
|
191
|
+
isRetriableRejection: () => isRetriableRejection
|
|
190
192
|
});
|
|
191
193
|
module.exports = __toCommonJS(index_exports);
|
|
192
194
|
init_cjs_shims();
|
|
@@ -248,6 +250,13 @@ function createDefaultLogger() {
|
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
// src/apply/rejection-taxonomy.ts
|
|
254
|
+
init_cjs_shims();
|
|
255
|
+
var RETRIABLE_REJECTION_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT"]);
|
|
256
|
+
function isRetriableRejection(code) {
|
|
257
|
+
return RETRIABLE_REJECTION_CODES.has(code);
|
|
258
|
+
}
|
|
259
|
+
|
|
251
260
|
// src/store/memory-server-store.ts
|
|
252
261
|
init_cjs_shims();
|
|
253
262
|
var import_core2 = require("@korajs/core");
|
|
@@ -269,6 +278,14 @@ function fieldTypeToSql(descriptor, dialect) {
|
|
|
269
278
|
return dialect === "postgres" ? "BIGINT" : "INTEGER";
|
|
270
279
|
case "array":
|
|
271
280
|
return dialect === "postgres" ? "JSONB" : "TEXT";
|
|
281
|
+
case "object":
|
|
282
|
+
return dialect === "postgres" ? "JSONB" : "TEXT";
|
|
283
|
+
case "json":
|
|
284
|
+
return dialect === "postgres" ? "JSONB" : "TEXT";
|
|
285
|
+
case "blob":
|
|
286
|
+
return "TEXT";
|
|
287
|
+
case "secret":
|
|
288
|
+
return "TEXT";
|
|
272
289
|
case "richtext":
|
|
273
290
|
return dialect === "postgres" ? "BYTEA" : "BLOB";
|
|
274
291
|
}
|
|
@@ -285,13 +302,13 @@ function generateCollectionDDL(name, collection, dialect) {
|
|
|
285
302
|
const columns = ["id TEXT PRIMARY KEY NOT NULL"];
|
|
286
303
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
287
304
|
const sqlType = fieldTypeToSql(descriptor, dialect);
|
|
288
|
-
let colDef = `${fieldName} ${sqlType}`;
|
|
305
|
+
let colDef = `${(0, import_core.quoteIdent)(fieldName)} ${sqlType}`;
|
|
289
306
|
if (descriptor.defaultValue !== void 0) {
|
|
290
307
|
colDef += ` DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`;
|
|
291
308
|
}
|
|
292
309
|
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
293
310
|
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
294
|
-
colDef += ` CHECK (${fieldName} IN (${values}))`;
|
|
311
|
+
colDef += ` CHECK (${(0, import_core.quoteIdent)(fieldName)} IN (${values}))`;
|
|
295
312
|
}
|
|
296
313
|
columns.push(colDef);
|
|
297
314
|
}
|
|
@@ -299,24 +316,26 @@ function generateCollectionDDL(name, collection, dialect) {
|
|
|
299
316
|
columns.push(`_created_at ${tsType} NOT NULL DEFAULT 0`);
|
|
300
317
|
columns.push(`_updated_at ${tsType} NOT NULL DEFAULT 0`);
|
|
301
318
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
302
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${name} (
|
|
319
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${(0, import_core.quoteIdent)(name)} (
|
|
303
320
|
${columns.join(",\n ")}
|
|
304
321
|
)`);
|
|
305
322
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
306
323
|
const sqlType = fieldTypeToSql(descriptor, dialect);
|
|
307
|
-
let colDef = `${fieldName} ${sqlType}`;
|
|
324
|
+
let colDef = `${(0, import_core.quoteIdent)(fieldName)} ${sqlType}`;
|
|
308
325
|
if (descriptor.defaultValue !== void 0) {
|
|
309
326
|
colDef += ` DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`;
|
|
310
327
|
}
|
|
311
328
|
statements.push(`--kora:safe-alter
|
|
312
|
-
ALTER TABLE ${name} ADD COLUMN ${colDef}`);
|
|
329
|
+
ALTER TABLE ${(0, import_core.quoteIdent)(name)} ADD COLUMN ${colDef}`);
|
|
313
330
|
}
|
|
314
331
|
for (const indexField of collection.indexes) {
|
|
315
332
|
statements.push(
|
|
316
|
-
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
|
|
333
|
+
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${(0, import_core.quoteIdent)(name)} (${(0, import_core.quoteIdent)(indexField)})`
|
|
317
334
|
);
|
|
318
335
|
}
|
|
319
|
-
statements.push(
|
|
336
|
+
statements.push(
|
|
337
|
+
`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${(0, import_core.quoteIdent)(name)} (_deleted)`
|
|
338
|
+
);
|
|
320
339
|
return statements;
|
|
321
340
|
}
|
|
322
341
|
function generateAllCollectionDDL(schema, dialect) {
|
|
@@ -952,7 +971,7 @@ var PostgresServerStore = class {
|
|
|
952
971
|
this.assertCollection(collection);
|
|
953
972
|
const schema = this.schema;
|
|
954
973
|
const collectionDef = schema.collections[collection];
|
|
955
|
-
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
974
|
+
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
|
|
956
975
|
const rows = await this.db.execute(query);
|
|
957
976
|
if (rows.length === 0) return null;
|
|
958
977
|
return this.deserializeRow(rows[0], collectionDef);
|
|
@@ -969,7 +988,7 @@ var PostgresServerStore = class {
|
|
|
969
988
|
}
|
|
970
989
|
}
|
|
971
990
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
972
|
-
const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`;
|
|
991
|
+
const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE ${whereClause}`;
|
|
973
992
|
const rows = await this.db.execute(query);
|
|
974
993
|
const cnt = rows[0]?.cnt;
|
|
975
994
|
return typeof cnt === "string" ? Number.parseInt(cnt, 10) : cnt ?? 0;
|
|
@@ -1056,7 +1075,7 @@ var PostgresServerStore = class {
|
|
|
1056
1075
|
);
|
|
1057
1076
|
} else {
|
|
1058
1077
|
await txOrDb.execute(
|
|
1059
|
-
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw(collection)} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1078
|
+
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1060
1079
|
);
|
|
1061
1080
|
}
|
|
1062
1081
|
}
|
|
@@ -1075,16 +1094,16 @@ var PostgresServerStore = class {
|
|
|
1075
1094
|
updatedAt,
|
|
1076
1095
|
0
|
|
1077
1096
|
];
|
|
1078
|
-
const columnsSql = import_drizzle_orm.sql.raw(allColumns.join(", "));
|
|
1097
|
+
const columnsSql = import_drizzle_orm.sql.raw(allColumns.map((c) => (0, import_core3.quoteIdent)(c)).join(", "));
|
|
1079
1098
|
const valuesSql = import_drizzle_orm.sql.join(
|
|
1080
1099
|
values.map((v) => import_drizzle_orm.sql`${v}`),
|
|
1081
1100
|
import_drizzle_orm.sql.raw(", ")
|
|
1082
1101
|
);
|
|
1083
1102
|
const updateSet = import_drizzle_orm.sql.raw(
|
|
1084
|
-
allColumns.slice(1).map((c) => `${c} = excluded.${c}`).join(", ")
|
|
1103
|
+
allColumns.slice(1).map((c) => `${(0, import_core3.quoteIdent)(c)} = excluded.${(0, import_core3.quoteIdent)(c)}`).join(", ")
|
|
1085
1104
|
);
|
|
1086
1105
|
await txOrDb.execute(
|
|
1087
|
-
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw(tableName)} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1106
|
+
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1088
1107
|
);
|
|
1089
1108
|
}
|
|
1090
1109
|
/**
|
|
@@ -1144,7 +1163,7 @@ var PostgresServerStore = class {
|
|
|
1144
1163
|
);
|
|
1145
1164
|
} else {
|
|
1146
1165
|
await this.db.execute(
|
|
1147
|
-
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw(collectionName)} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1166
|
+
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collectionName))} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1148
1167
|
);
|
|
1149
1168
|
}
|
|
1150
1169
|
}
|
|
@@ -1157,10 +1176,12 @@ var PostgresServerStore = class {
|
|
|
1157
1176
|
options?.where ?? {},
|
|
1158
1177
|
options?.includeDeleted ?? false
|
|
1159
1178
|
);
|
|
1160
|
-
const parts = [
|
|
1179
|
+
const parts = [
|
|
1180
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE ${whereClause}`
|
|
1181
|
+
];
|
|
1161
1182
|
if (options?.orderBy) {
|
|
1162
1183
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1163
|
-
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
1184
|
+
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${(0, import_core3.quoteIdent)(options.orderBy)} ${dir}`));
|
|
1164
1185
|
}
|
|
1165
1186
|
if (options?.limit !== void 0) {
|
|
1166
1187
|
parts.push(import_drizzle_orm.sql` LIMIT ${options.limit}`);
|
|
@@ -1176,7 +1197,7 @@ var PostgresServerStore = class {
|
|
|
1176
1197
|
conditions.push(import_drizzle_orm.sql.raw("_deleted = 0"));
|
|
1177
1198
|
}
|
|
1178
1199
|
for (const [key, value] of Object.entries(where)) {
|
|
1179
|
-
conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw(key)} = ${value}`);
|
|
1200
|
+
conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(key))} = ${value}`);
|
|
1180
1201
|
}
|
|
1181
1202
|
if (conditions.length === 0) {
|
|
1182
1203
|
return import_drizzle_orm.sql.raw("1 = 1");
|
|
@@ -1528,7 +1549,7 @@ var SqliteServerStore = class {
|
|
|
1528
1549
|
this.assertCollection(collection);
|
|
1529
1550
|
const schema = this.schema;
|
|
1530
1551
|
const collectionDef = schema.collections[collection];
|
|
1531
|
-
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
1552
|
+
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
|
|
1532
1553
|
const rows = this.db.all(query);
|
|
1533
1554
|
if (rows.length === 0) return null;
|
|
1534
1555
|
return this.deserializeRow(rows[0], collectionDef);
|
|
@@ -1544,7 +1565,7 @@ var SqliteServerStore = class {
|
|
|
1544
1565
|
}
|
|
1545
1566
|
}
|
|
1546
1567
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
1547
|
-
const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`;
|
|
1568
|
+
const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE ${whereClause}`;
|
|
1548
1569
|
const rows = this.db.all(query);
|
|
1549
1570
|
return rows[0]?.cnt ?? 0;
|
|
1550
1571
|
}
|
|
@@ -1621,7 +1642,7 @@ var SqliteServerStore = class {
|
|
|
1621
1642
|
);
|
|
1622
1643
|
} else {
|
|
1623
1644
|
txOrDb.run(
|
|
1624
|
-
import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw(collection)} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1645
|
+
import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1625
1646
|
);
|
|
1626
1647
|
}
|
|
1627
1648
|
}
|
|
@@ -1642,16 +1663,16 @@ var SqliteServerStore = class {
|
|
|
1642
1663
|
0
|
|
1643
1664
|
// _deleted = false
|
|
1644
1665
|
];
|
|
1645
|
-
const columnsSql = import_drizzle_orm2.sql.raw(allColumns.join(", "));
|
|
1666
|
+
const columnsSql = import_drizzle_orm2.sql.raw(allColumns.map((c) => (0, import_core4.quoteIdent)(c)).join(", "));
|
|
1646
1667
|
const valuesSql = import_drizzle_orm2.sql.join(
|
|
1647
1668
|
values.map((v) => import_drizzle_orm2.sql`${v}`),
|
|
1648
1669
|
import_drizzle_orm2.sql.raw(", ")
|
|
1649
1670
|
);
|
|
1650
1671
|
const updateSet = import_drizzle_orm2.sql.raw(
|
|
1651
|
-
allColumns.slice(1).map((c) => `${c} = excluded.${c}`).join(", ")
|
|
1672
|
+
allColumns.slice(1).map((c) => `${(0, import_core4.quoteIdent)(c)} = excluded.${(0, import_core4.quoteIdent)(c)}`).join(", ")
|
|
1652
1673
|
);
|
|
1653
1674
|
txOrDb.run(
|
|
1654
|
-
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw(tableName)} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1675
|
+
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1655
1676
|
);
|
|
1656
1677
|
}
|
|
1657
1678
|
/**
|
|
@@ -1709,7 +1730,7 @@ var SqliteServerStore = class {
|
|
|
1709
1730
|
);
|
|
1710
1731
|
} else {
|
|
1711
1732
|
tx.run(
|
|
1712
|
-
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw(collectionName)} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1733
|
+
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collectionName))} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1713
1734
|
);
|
|
1714
1735
|
}
|
|
1715
1736
|
}
|
|
@@ -1723,10 +1744,12 @@ var SqliteServerStore = class {
|
|
|
1723
1744
|
options?.where ?? {},
|
|
1724
1745
|
options?.includeDeleted ?? false
|
|
1725
1746
|
);
|
|
1726
|
-
const parts = [
|
|
1747
|
+
const parts = [
|
|
1748
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE ${whereClause}`
|
|
1749
|
+
];
|
|
1727
1750
|
if (options?.orderBy) {
|
|
1728
1751
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1729
|
-
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
1752
|
+
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${(0, import_core4.quoteIdent)(options.orderBy)} ${dir}`));
|
|
1730
1753
|
}
|
|
1731
1754
|
if (options?.limit !== void 0) {
|
|
1732
1755
|
parts.push(import_drizzle_orm2.sql` LIMIT ${options.limit}`);
|
|
@@ -1742,7 +1765,7 @@ var SqliteServerStore = class {
|
|
|
1742
1765
|
conditions.push(import_drizzle_orm2.sql.raw("_deleted = 0"));
|
|
1743
1766
|
}
|
|
1744
1767
|
for (const [key, value] of Object.entries(where)) {
|
|
1745
|
-
conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw(key)} = ${value}`);
|
|
1768
|
+
conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(key))} = ${value}`);
|
|
1746
1769
|
}
|
|
1747
1770
|
if (conditions.length === 0) {
|
|
1748
1771
|
return import_drizzle_orm2.sql.raw("1 = 1");
|
|
@@ -2189,12 +2212,14 @@ async function applyServerOperation(store, op, relationLookup) {
|
|
|
2189
2212
|
const lookup = relationLookup ?? (schema ? (0, import_merge2.buildMergeRelationLookup)(schema) : /* @__PURE__ */ new Map());
|
|
2190
2213
|
const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
|
|
2191
2214
|
if (!constraintCheck.valid) {
|
|
2215
|
+
const code = constraintCheck.code ?? "CONSTRAINT_VIOLATION";
|
|
2192
2216
|
return {
|
|
2193
2217
|
result: "skipped",
|
|
2194
2218
|
appliedOperations: [],
|
|
2195
2219
|
rejection: {
|
|
2196
|
-
code
|
|
2197
|
-
message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint
|
|
2220
|
+
code,
|
|
2221
|
+
message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`,
|
|
2222
|
+
retriable: isRetriableRejection(code)
|
|
2198
2223
|
}
|
|
2199
2224
|
};
|
|
2200
2225
|
}
|
|
@@ -2207,7 +2232,8 @@ async function applyServerOperation(store, op, relationLookup) {
|
|
|
2207
2232
|
appliedOperations: [],
|
|
2208
2233
|
rejection: {
|
|
2209
2234
|
code: "REFERENTIAL_INTEGRITY",
|
|
2210
|
-
message: `Operation "${op.id}" violates referential integrity on "${op.collection}"
|
|
2235
|
+
message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`,
|
|
2236
|
+
retriable: isRetriableRejection("REFERENTIAL_INTEGRITY")
|
|
2211
2237
|
}
|
|
2212
2238
|
};
|
|
2213
2239
|
}
|
|
@@ -2421,6 +2447,8 @@ var ClientSession = class {
|
|
|
2421
2447
|
maxOperationBytes;
|
|
2422
2448
|
maxOpsPerMinute;
|
|
2423
2449
|
rateLimiter;
|
|
2450
|
+
validateOperation;
|
|
2451
|
+
koraContext;
|
|
2424
2452
|
constructor(options) {
|
|
2425
2453
|
this.sessionId = options.sessionId;
|
|
2426
2454
|
this.transport = options.transport;
|
|
@@ -2445,6 +2473,8 @@ var ClientSession = class {
|
|
|
2445
2473
|
this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
|
|
2446
2474
|
this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
|
|
2447
2475
|
this.rateLimiter = new SessionRateLimiter(this.maxOpsPerMinute);
|
|
2476
|
+
this.validateOperation = options.validateOperation ?? null;
|
|
2477
|
+
this.koraContext = options.koraContext ?? null;
|
|
2448
2478
|
}
|
|
2449
2479
|
/**
|
|
2450
2480
|
* Start handling messages from the client transport.
|
|
@@ -2691,10 +2721,39 @@ var ClientSession = class {
|
|
|
2691
2721
|
);
|
|
2692
2722
|
continue;
|
|
2693
2723
|
}
|
|
2724
|
+
if (this.validateOperation && this.koraContext) {
|
|
2725
|
+
let decision;
|
|
2726
|
+
try {
|
|
2727
|
+
decision = await this.validateOperation(op, {
|
|
2728
|
+
auth: this.authContext,
|
|
2729
|
+
kora: this.koraContext
|
|
2730
|
+
});
|
|
2731
|
+
} catch (error) {
|
|
2732
|
+
const message = error instanceof Error ? error.message : "validator error";
|
|
2733
|
+
this.sendOperationRejected(op, "VALIDATION_ERROR", `Validator threw: ${message}`, true);
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
if (decision.action === "reject") {
|
|
2737
|
+
this.sendOperationRejected(
|
|
2738
|
+
op,
|
|
2739
|
+
decision.code,
|
|
2740
|
+
decision.message,
|
|
2741
|
+
decision.retriable ?? isRetriableRejection(decision.code)
|
|
2742
|
+
);
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
if (decision.action === "ignore") {
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2694
2749
|
try {
|
|
2695
2750
|
const applyResult = await applyServerOperation(this.store, op);
|
|
2696
2751
|
if (applyResult.rejection) {
|
|
2697
|
-
this.sendError(
|
|
2752
|
+
this.sendError(
|
|
2753
|
+
applyResult.rejection.code,
|
|
2754
|
+
applyResult.rejection.message,
|
|
2755
|
+
applyResult.rejection.retriable
|
|
2756
|
+
);
|
|
2698
2757
|
continue;
|
|
2699
2758
|
}
|
|
2700
2759
|
if (applyResult.result === "applied") {
|
|
@@ -2813,6 +2872,25 @@ var ClientSession = class {
|
|
|
2813
2872
|
};
|
|
2814
2873
|
this.sendToClient(errorMsg);
|
|
2815
2874
|
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Reject one specific client operation, tied to its id, so the submitter can
|
|
2877
|
+
* divert it out of its pending queue into a durable rejected store rather than
|
|
2878
|
+
* losing it or retrying forever. The op is NOT materialized, so no other
|
|
2879
|
+
* replica ever sees it.
|
|
2880
|
+
*/
|
|
2881
|
+
sendOperationRejected(op, code, message, retriable) {
|
|
2882
|
+
const rejectedMsg = {
|
|
2883
|
+
type: "operation-rejected",
|
|
2884
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2885
|
+
operationId: op.id,
|
|
2886
|
+
collection: op.collection,
|
|
2887
|
+
recordId: op.recordId,
|
|
2888
|
+
code,
|
|
2889
|
+
message,
|
|
2890
|
+
retriable
|
|
2891
|
+
};
|
|
2892
|
+
this.sendToClient(rejectedMsg);
|
|
2893
|
+
}
|
|
2816
2894
|
setSerializerWireFormat(format) {
|
|
2817
2895
|
if (typeof this.serializer.setWireFormat === "function") {
|
|
2818
2896
|
this.serializer.setWireFormat(format);
|
|
@@ -2834,7 +2912,7 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
2834
2912
|
|
|
2835
2913
|
// src/server/kora-sync-server.ts
|
|
2836
2914
|
init_cjs_shims();
|
|
2837
|
-
var
|
|
2915
|
+
var import_core13 = require("@korajs/core");
|
|
2838
2916
|
var import_internal2 = require("@korajs/core/internal");
|
|
2839
2917
|
var import_sync5 = require("@korajs/sync");
|
|
2840
2918
|
|
|
@@ -3196,6 +3274,311 @@ var YjsDocRelay = class {
|
|
|
3196
3274
|
}
|
|
3197
3275
|
};
|
|
3198
3276
|
|
|
3277
|
+
// src/server/route-context.ts
|
|
3278
|
+
init_cjs_shims();
|
|
3279
|
+
var import_core12 = require("@korajs/core");
|
|
3280
|
+
|
|
3281
|
+
// src/apply/route-predicate.ts
|
|
3282
|
+
init_cjs_shims();
|
|
3283
|
+
function evaluateField(actual, operators) {
|
|
3284
|
+
if ("$eq" in operators && !Object.is(actual, operators.$eq)) {
|
|
3285
|
+
return false;
|
|
3286
|
+
}
|
|
3287
|
+
if ("$ne" in operators && Object.is(actual, operators.$ne)) {
|
|
3288
|
+
return false;
|
|
3289
|
+
}
|
|
3290
|
+
if (operators.$in !== void 0 && !operators.$in.some((candidate) => Object.is(actual, candidate))) {
|
|
3291
|
+
return false;
|
|
3292
|
+
}
|
|
3293
|
+
const numericChecks = [
|
|
3294
|
+
["$lt", (a, b) => a < b],
|
|
3295
|
+
["$lte", (a, b) => a <= b],
|
|
3296
|
+
["$gt", (a, b) => a > b],
|
|
3297
|
+
["$gte", (a, b) => a >= b]
|
|
3298
|
+
];
|
|
3299
|
+
for (const [operator, compare] of numericChecks) {
|
|
3300
|
+
const bound = operators[operator];
|
|
3301
|
+
if (bound === void 0) {
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
if (typeof actual !== "number" || typeof bound !== "number") {
|
|
3305
|
+
return false;
|
|
3306
|
+
}
|
|
3307
|
+
if (!compare(actual, bound)) {
|
|
3308
|
+
return false;
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
return true;
|
|
3312
|
+
}
|
|
3313
|
+
function evaluateRoutePredicate(record, predicate) {
|
|
3314
|
+
const fields = Object.keys(predicate);
|
|
3315
|
+
if (fields.length === 0) {
|
|
3316
|
+
return true;
|
|
3317
|
+
}
|
|
3318
|
+
if (record === null) {
|
|
3319
|
+
return false;
|
|
3320
|
+
}
|
|
3321
|
+
for (const field of fields) {
|
|
3322
|
+
const operators = predicate[field];
|
|
3323
|
+
if (operators && !evaluateField(record[field], operators)) {
|
|
3324
|
+
return false;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
return true;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// src/server/route-context.ts
|
|
3331
|
+
function recordMatchesScope(collection, record, scope) {
|
|
3332
|
+
const collectionScope = scope[collection];
|
|
3333
|
+
if (!collectionScope) {
|
|
3334
|
+
return false;
|
|
3335
|
+
}
|
|
3336
|
+
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
3337
|
+
if (record[field] !== expected) {
|
|
3338
|
+
return false;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
return true;
|
|
3342
|
+
}
|
|
3343
|
+
function pickFields(record, keys) {
|
|
3344
|
+
const picked = {};
|
|
3345
|
+
for (const key of keys) {
|
|
3346
|
+
if (key in record) {
|
|
3347
|
+
picked[key] = record[key];
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return picked;
|
|
3351
|
+
}
|
|
3352
|
+
async function buildRouteOperation(store, mutation) {
|
|
3353
|
+
const nodeId = store.getNodeId();
|
|
3354
|
+
const clock = new import_core12.HybridLogicalClock(nodeId);
|
|
3355
|
+
const schemaVersion = store.getSchema()?.version ?? 1;
|
|
3356
|
+
const sequenceNumber = nextServerSequenceNumber(store);
|
|
3357
|
+
if (mutation.type === "insert") {
|
|
3358
|
+
return (0, import_core12.createOperation)(
|
|
3359
|
+
{
|
|
3360
|
+
nodeId,
|
|
3361
|
+
type: "insert",
|
|
3362
|
+
collection: mutation.collection,
|
|
3363
|
+
recordId: mutation.recordId ?? (0, import_core12.generateUUIDv7)(),
|
|
3364
|
+
data: mutation.data ?? {},
|
|
3365
|
+
previousData: null,
|
|
3366
|
+
sequenceNumber,
|
|
3367
|
+
causalDeps: [],
|
|
3368
|
+
schemaVersion
|
|
3369
|
+
},
|
|
3370
|
+
clock
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
if (!mutation.recordId) {
|
|
3374
|
+
throw new RouteMutationError(
|
|
3375
|
+
"MISSING_RECORD_ID",
|
|
3376
|
+
`A ${mutation.type} mutation on "${mutation.collection}" requires a recordId.`
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
const current = await store.findRecord(mutation.collection, mutation.recordId);
|
|
3380
|
+
if (mutation.type === "update") {
|
|
3381
|
+
const rawData = mutation.data ?? {};
|
|
3382
|
+
const data = {};
|
|
3383
|
+
const atomicOps = {};
|
|
3384
|
+
for (const key of Object.keys(rawData)) {
|
|
3385
|
+
const value = rawData[key];
|
|
3386
|
+
if ((0, import_core12.isAtomicOp)(value)) {
|
|
3387
|
+
data[key] = (0, import_core12.resolveAtomicOp)(current ? current[key] : void 0, value);
|
|
3388
|
+
atomicOps[key] = (0, import_core12.toAtomicOp)(value);
|
|
3389
|
+
} else {
|
|
3390
|
+
data[key] = value;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
return (0, import_core12.createOperation)(
|
|
3394
|
+
{
|
|
3395
|
+
nodeId,
|
|
3396
|
+
type: "update",
|
|
3397
|
+
collection: mutation.collection,
|
|
3398
|
+
recordId: mutation.recordId,
|
|
3399
|
+
data,
|
|
3400
|
+
previousData: current ? pickFields(current, Object.keys(rawData)) : {},
|
|
3401
|
+
sequenceNumber,
|
|
3402
|
+
causalDeps: [],
|
|
3403
|
+
schemaVersion,
|
|
3404
|
+
...Object.keys(atomicOps).length > 0 ? { atomicOps } : {}
|
|
3405
|
+
},
|
|
3406
|
+
clock
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
return (0, import_core12.createOperation)(
|
|
3410
|
+
{
|
|
3411
|
+
nodeId,
|
|
3412
|
+
type: "delete",
|
|
3413
|
+
collection: mutation.collection,
|
|
3414
|
+
recordId: mutation.recordId,
|
|
3415
|
+
data: null,
|
|
3416
|
+
previousData: current ? { ...current } : null,
|
|
3417
|
+
sequenceNumber,
|
|
3418
|
+
causalDeps: [],
|
|
3419
|
+
schemaVersion
|
|
3420
|
+
},
|
|
3421
|
+
clock
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
var RouteMutationError = class extends Error {
|
|
3425
|
+
constructor(code, message) {
|
|
3426
|
+
super(message);
|
|
3427
|
+
this.code = code;
|
|
3428
|
+
this.name = "RouteMutationError";
|
|
3429
|
+
}
|
|
3430
|
+
code;
|
|
3431
|
+
};
|
|
3432
|
+
function createRouteContext(server, store) {
|
|
3433
|
+
let mutationTail = Promise.resolve();
|
|
3434
|
+
function serialize(work) {
|
|
3435
|
+
const run = mutationTail.then(work, work);
|
|
3436
|
+
mutationTail = run.then(
|
|
3437
|
+
() => void 0,
|
|
3438
|
+
() => void 0
|
|
3439
|
+
);
|
|
3440
|
+
return run;
|
|
3441
|
+
}
|
|
3442
|
+
async function prepareMutation(mutation, scope) {
|
|
3443
|
+
let op;
|
|
3444
|
+
try {
|
|
3445
|
+
op = await buildRouteOperation(store, mutation);
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
if (error instanceof RouteMutationError) {
|
|
3448
|
+
return {
|
|
3449
|
+
ok: false,
|
|
3450
|
+
rejection: {
|
|
3451
|
+
ok: false,
|
|
3452
|
+
code: error.code,
|
|
3453
|
+
message: error.message,
|
|
3454
|
+
retriable: isRetriableRejection(error.code)
|
|
3455
|
+
}
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
throw error;
|
|
3459
|
+
}
|
|
3460
|
+
if (scope && !operationMatchesScopes(op, scope)) {
|
|
3461
|
+
return {
|
|
3462
|
+
ok: false,
|
|
3463
|
+
rejection: {
|
|
3464
|
+
ok: false,
|
|
3465
|
+
code: "SCOPE_VIOLATION",
|
|
3466
|
+
message: `Mutation on "${mutation.collection}" is outside the provided scope.`,
|
|
3467
|
+
retriable: isRetriableRejection("SCOPE_VIOLATION")
|
|
3468
|
+
}
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
return { ok: true, op };
|
|
3472
|
+
}
|
|
3473
|
+
async function commitOperation(op, collection) {
|
|
3474
|
+
const result = await server.applyLocalOperation(op);
|
|
3475
|
+
if (result.result !== "applied") {
|
|
3476
|
+
const code = result.rejection?.code ?? "NOT_APPLIED";
|
|
3477
|
+
return {
|
|
3478
|
+
ok: false,
|
|
3479
|
+
code,
|
|
3480
|
+
message: result.rejection?.message ?? `Operation on "${collection}" was not applied (${result.result}).`,
|
|
3481
|
+
// Prefer the pipeline's own classification when present; fall back
|
|
3482
|
+
// to classifying the code so this stays correct if new codes appear.
|
|
3483
|
+
retriable: result.rejection?.retriable ?? isRetriableRejection(code)
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
const record = await store.findRecord(collection, op.recordId);
|
|
3487
|
+
return { ok: true, operation: op, record };
|
|
3488
|
+
}
|
|
3489
|
+
async function rawApply(mutation, scope) {
|
|
3490
|
+
const prepared = await prepareMutation(mutation, scope);
|
|
3491
|
+
if (!prepared.ok) {
|
|
3492
|
+
return prepared.rejection;
|
|
3493
|
+
}
|
|
3494
|
+
return commitOperation(prepared.op, mutation.collection);
|
|
3495
|
+
}
|
|
3496
|
+
return {
|
|
3497
|
+
apply(mutation, options) {
|
|
3498
|
+
return serialize(() => rawApply(mutation, options?.scope));
|
|
3499
|
+
},
|
|
3500
|
+
applyConditional(spec, options) {
|
|
3501
|
+
return serialize(async () => {
|
|
3502
|
+
if (spec.idempotencyKey) {
|
|
3503
|
+
const existing = await store.findRecord(
|
|
3504
|
+
spec.idempotencyKey.collection,
|
|
3505
|
+
spec.idempotencyKey.id
|
|
3506
|
+
);
|
|
3507
|
+
if (existing) {
|
|
3508
|
+
return { ok: true, idempotent: true, operations: [], records: [existing] };
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
if (spec.if && Object.keys(spec.if).length > 0) {
|
|
3512
|
+
const target = await store.findRecord(spec.collection, spec.id);
|
|
3513
|
+
if (!evaluateRoutePredicate(target, spec.if)) {
|
|
3514
|
+
const code = spec.reject?.code ?? "CONDITION_NOT_MET";
|
|
3515
|
+
return {
|
|
3516
|
+
ok: false,
|
|
3517
|
+
code,
|
|
3518
|
+
message: spec.reject?.message ?? `Admission condition not met on "${spec.collection}" record "${spec.id}".`,
|
|
3519
|
+
retriable: false
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
const mutations = [];
|
|
3524
|
+
if (spec.update !== void 0 && spec.update !== null) {
|
|
3525
|
+
mutations.push({
|
|
3526
|
+
collection: spec.collection,
|
|
3527
|
+
type: "update",
|
|
3528
|
+
recordId: spec.id,
|
|
3529
|
+
data: spec.update
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
if (spec.also) {
|
|
3533
|
+
mutations.push(...spec.also);
|
|
3534
|
+
}
|
|
3535
|
+
const prepared = [];
|
|
3536
|
+
for (const mutation of mutations) {
|
|
3537
|
+
const p = await prepareMutation(mutation, options?.scope);
|
|
3538
|
+
if (!p.ok) {
|
|
3539
|
+
return p.rejection;
|
|
3540
|
+
}
|
|
3541
|
+
prepared.push({ op: p.op, collection: mutation.collection });
|
|
3542
|
+
}
|
|
3543
|
+
const operations2 = [];
|
|
3544
|
+
const records = [];
|
|
3545
|
+
for (const { op, collection } of prepared) {
|
|
3546
|
+
const result = await commitOperation(op, collection);
|
|
3547
|
+
if (!result.ok) {
|
|
3548
|
+
return {
|
|
3549
|
+
ok: false,
|
|
3550
|
+
code: result.code,
|
|
3551
|
+
message: result.message,
|
|
3552
|
+
retriable: result.retriable
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
operations2.push(result.operation);
|
|
3556
|
+
records.push(result.record);
|
|
3557
|
+
}
|
|
3558
|
+
return { ok: true, idempotent: false, operations: operations2, records };
|
|
3559
|
+
});
|
|
3560
|
+
},
|
|
3561
|
+
async query(collection, options) {
|
|
3562
|
+
const { scope, ...queryOptions } = options ?? {};
|
|
3563
|
+
const records = await store.queryCollection(collection, queryOptions);
|
|
3564
|
+
if (!scope) {
|
|
3565
|
+
return records;
|
|
3566
|
+
}
|
|
3567
|
+
return records.filter((record) => recordMatchesScope(collection, record, scope));
|
|
3568
|
+
},
|
|
3569
|
+
async findById(collection, id, options) {
|
|
3570
|
+
const record = await store.findRecord(collection, id);
|
|
3571
|
+
if (!record) {
|
|
3572
|
+
return null;
|
|
3573
|
+
}
|
|
3574
|
+
if (options?.scope && !recordMatchesScope(collection, record, options.scope)) {
|
|
3575
|
+
return null;
|
|
3576
|
+
}
|
|
3577
|
+
return record;
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3199
3582
|
// src/server/kora-sync-server.ts
|
|
3200
3583
|
var DEFAULT_MAX_CONNECTIONS = 0;
|
|
3201
3584
|
var DEFAULT_BATCH_SIZE2 = 100;
|
|
@@ -3220,10 +3603,16 @@ var KoraSyncServer = class {
|
|
|
3220
3603
|
yjsDocRelay = new YjsDocRelay();
|
|
3221
3604
|
blobChunkRelay;
|
|
3222
3605
|
persistBlobChunk;
|
|
3606
|
+
maxOperationBytes;
|
|
3607
|
+
maxOpsPerMinute;
|
|
3608
|
+
validateOperation;
|
|
3609
|
+
koraContext;
|
|
3223
3610
|
sessions = /* @__PURE__ */ new Map();
|
|
3224
3611
|
httpClients = /* @__PURE__ */ new Map();
|
|
3225
3612
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
3226
|
-
|
|
3613
|
+
// Informational value reported by getStatus(). Keep in sync with the
|
|
3614
|
+
// @korajs/server package version on release; it is not used for protocol negotiation.
|
|
3615
|
+
serverVersion = "1.0.0-beta.0";
|
|
3227
3616
|
wsServer = null;
|
|
3228
3617
|
running = false;
|
|
3229
3618
|
constructor(config) {
|
|
@@ -3246,10 +3635,22 @@ var KoraSyncServer = class {
|
|
|
3246
3635
|
this.metrics.setSchemaVersion(this.schemaVersion);
|
|
3247
3636
|
this.blobChunkRelay = new BlobChunkRelay(config.resolveBlobChunk);
|
|
3248
3637
|
this.persistBlobChunk = config.persistBlobChunk ?? null;
|
|
3638
|
+
this.maxOperationBytes = config.maxOperationBytes;
|
|
3639
|
+
this.maxOpsPerMinute = config.maxOpsPerMinute;
|
|
3640
|
+
this.validateOperation = config.validateOperation;
|
|
3641
|
+
this.koraContext = createRouteContext(this, this.store);
|
|
3249
3642
|
if (!this.emitter) {
|
|
3250
3643
|
this.emitter = new import_internal2.SimpleEventEmitter();
|
|
3251
3644
|
}
|
|
3252
3645
|
}
|
|
3646
|
+
/**
|
|
3647
|
+
* The trusted, scoped data-plane context (`apply` / `query` / `findById`).
|
|
3648
|
+
* Shared with the production server so HTTP routes, the operation validator,
|
|
3649
|
+
* and `server.kora` are all the same object over the same pipeline.
|
|
3650
|
+
*/
|
|
3651
|
+
getKoraContext() {
|
|
3652
|
+
return this.koraContext;
|
|
3653
|
+
}
|
|
3253
3654
|
/**
|
|
3254
3655
|
* Subscribe to session-level events for metrics collection and logging.
|
|
3255
3656
|
* Called when a new session is created.
|
|
@@ -3318,10 +3719,10 @@ var KoraSyncServer = class {
|
|
|
3318
3719
|
*/
|
|
3319
3720
|
async start(wsServerImpl) {
|
|
3320
3721
|
if (this.running) {
|
|
3321
|
-
throw new
|
|
3722
|
+
throw new import_core13.SyncError("Server is already running", { port: this.port });
|
|
3322
3723
|
}
|
|
3323
3724
|
if (!wsServerImpl && this.port === void 0) {
|
|
3324
|
-
throw new
|
|
3725
|
+
throw new import_core13.SyncError(
|
|
3325
3726
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
3326
3727
|
{}
|
|
3327
3728
|
);
|
|
@@ -3431,7 +3832,7 @@ var KoraSyncServer = class {
|
|
|
3431
3832
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
3432
3833
|
transport.send({
|
|
3433
3834
|
type: "error",
|
|
3434
|
-
messageId: (0,
|
|
3835
|
+
messageId: (0, import_core13.generateUUIDv7)(),
|
|
3435
3836
|
code: "MAX_CONNECTIONS",
|
|
3436
3837
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
3437
3838
|
retriable: true
|
|
@@ -3444,12 +3845,12 @@ var KoraSyncServer = class {
|
|
|
3444
3845
|
event: "connection.rejected",
|
|
3445
3846
|
details: { reason: "max_connections", max: this.maxConnections }
|
|
3446
3847
|
});
|
|
3447
|
-
throw new
|
|
3848
|
+
throw new import_core13.SyncError("Maximum connections reached", {
|
|
3448
3849
|
current: this.sessions.size,
|
|
3449
3850
|
max: this.maxConnections
|
|
3450
3851
|
});
|
|
3451
3852
|
}
|
|
3452
|
-
const sessionId = (0,
|
|
3853
|
+
const sessionId = (0, import_core13.generateUUIDv7)();
|
|
3453
3854
|
this.metrics.recordConnection(sessionId);
|
|
3454
3855
|
const sessionEmitter = new import_internal2.SimpleEventEmitter();
|
|
3455
3856
|
sessionEmitter.on("sync:connected", (event) => {
|
|
@@ -3521,6 +3922,11 @@ var KoraSyncServer = class {
|
|
|
3521
3922
|
this.blobChunkRelay.handleResponse(sourceSessionId, message);
|
|
3522
3923
|
},
|
|
3523
3924
|
...this.persistBlobChunk ? { persistBlobChunk: this.persistBlobChunk } : {},
|
|
3925
|
+
// Only forward when configured, so an unset server value leaves the
|
|
3926
|
+
// session on its own documented default rather than `undefined`.
|
|
3927
|
+
...this.maxOperationBytes !== void 0 ? { maxOperationBytes: this.maxOperationBytes } : {},
|
|
3928
|
+
...this.maxOpsPerMinute !== void 0 ? { maxOpsPerMinute: this.maxOpsPerMinute } : {},
|
|
3929
|
+
...this.validateOperation ? { validateOperation: this.validateOperation, koraContext: this.koraContext } : {},
|
|
3524
3930
|
onClose: (sid) => {
|
|
3525
3931
|
this.handleSessionClose(sid);
|
|
3526
3932
|
}
|
|
@@ -3607,7 +4013,7 @@ var KoraSyncServer = class {
|
|
|
3607
4013
|
const records = await this.store.materializeCollection(name);
|
|
3608
4014
|
for (const record of records) {
|
|
3609
4015
|
for (const value of Object.values(record)) {
|
|
3610
|
-
if ((0,
|
|
4016
|
+
if ((0, import_core13.isBlobRef)(value)) {
|
|
3611
4017
|
refs.push(value);
|
|
3612
4018
|
}
|
|
3613
4019
|
}
|
|
@@ -3787,162 +4193,6 @@ function createKoraServer(config) {
|
|
|
3787
4193
|
|
|
3788
4194
|
// src/server/production-server.ts
|
|
3789
4195
|
init_cjs_shims();
|
|
3790
|
-
|
|
3791
|
-
// src/server/route-context.ts
|
|
3792
|
-
init_cjs_shims();
|
|
3793
|
-
var import_core13 = require("@korajs/core");
|
|
3794
|
-
function recordMatchesScope(collection, record, scope) {
|
|
3795
|
-
const collectionScope = scope[collection];
|
|
3796
|
-
if (!collectionScope) {
|
|
3797
|
-
return false;
|
|
3798
|
-
}
|
|
3799
|
-
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
3800
|
-
if (record[field] !== expected) {
|
|
3801
|
-
return false;
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
return true;
|
|
3805
|
-
}
|
|
3806
|
-
function pickFields(record, keys) {
|
|
3807
|
-
const picked = {};
|
|
3808
|
-
for (const key of keys) {
|
|
3809
|
-
if (key in record) {
|
|
3810
|
-
picked[key] = record[key];
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
return picked;
|
|
3814
|
-
}
|
|
3815
|
-
async function buildRouteOperation(store, mutation) {
|
|
3816
|
-
const nodeId = store.getNodeId();
|
|
3817
|
-
const clock = new import_core13.HybridLogicalClock(nodeId);
|
|
3818
|
-
const schemaVersion = store.getSchema()?.version ?? 1;
|
|
3819
|
-
const sequenceNumber = nextServerSequenceNumber(store);
|
|
3820
|
-
if (mutation.type === "insert") {
|
|
3821
|
-
return (0, import_core13.createOperation)(
|
|
3822
|
-
{
|
|
3823
|
-
nodeId,
|
|
3824
|
-
type: "insert",
|
|
3825
|
-
collection: mutation.collection,
|
|
3826
|
-
recordId: mutation.recordId ?? (0, import_core13.generateUUIDv7)(),
|
|
3827
|
-
data: mutation.data ?? {},
|
|
3828
|
-
previousData: null,
|
|
3829
|
-
sequenceNumber,
|
|
3830
|
-
causalDeps: [],
|
|
3831
|
-
schemaVersion
|
|
3832
|
-
},
|
|
3833
|
-
clock
|
|
3834
|
-
);
|
|
3835
|
-
}
|
|
3836
|
-
if (!mutation.recordId) {
|
|
3837
|
-
throw new RouteMutationError(
|
|
3838
|
-
"MISSING_RECORD_ID",
|
|
3839
|
-
`A ${mutation.type} mutation on "${mutation.collection}" requires a recordId.`
|
|
3840
|
-
);
|
|
3841
|
-
}
|
|
3842
|
-
const current = await store.findRecord(mutation.collection, mutation.recordId);
|
|
3843
|
-
if (mutation.type === "update") {
|
|
3844
|
-
const data = mutation.data ?? {};
|
|
3845
|
-
return (0, import_core13.createOperation)(
|
|
3846
|
-
{
|
|
3847
|
-
nodeId,
|
|
3848
|
-
type: "update",
|
|
3849
|
-
collection: mutation.collection,
|
|
3850
|
-
recordId: mutation.recordId,
|
|
3851
|
-
data,
|
|
3852
|
-
previousData: current ? pickFields(current, Object.keys(data)) : {},
|
|
3853
|
-
sequenceNumber,
|
|
3854
|
-
causalDeps: [],
|
|
3855
|
-
schemaVersion
|
|
3856
|
-
},
|
|
3857
|
-
clock
|
|
3858
|
-
);
|
|
3859
|
-
}
|
|
3860
|
-
return (0, import_core13.createOperation)(
|
|
3861
|
-
{
|
|
3862
|
-
nodeId,
|
|
3863
|
-
type: "delete",
|
|
3864
|
-
collection: mutation.collection,
|
|
3865
|
-
recordId: mutation.recordId,
|
|
3866
|
-
data: null,
|
|
3867
|
-
previousData: current ? { ...current } : null,
|
|
3868
|
-
sequenceNumber,
|
|
3869
|
-
causalDeps: [],
|
|
3870
|
-
schemaVersion
|
|
3871
|
-
},
|
|
3872
|
-
clock
|
|
3873
|
-
);
|
|
3874
|
-
}
|
|
3875
|
-
var RouteMutationError = class extends Error {
|
|
3876
|
-
constructor(code, message) {
|
|
3877
|
-
super(message);
|
|
3878
|
-
this.code = code;
|
|
3879
|
-
this.name = "RouteMutationError";
|
|
3880
|
-
}
|
|
3881
|
-
code;
|
|
3882
|
-
};
|
|
3883
|
-
function createRouteContext(server, store) {
|
|
3884
|
-
let mutationTail = Promise.resolve();
|
|
3885
|
-
function serialize(work) {
|
|
3886
|
-
const run = mutationTail.then(work, work);
|
|
3887
|
-
mutationTail = run.then(
|
|
3888
|
-
() => void 0,
|
|
3889
|
-
() => void 0
|
|
3890
|
-
);
|
|
3891
|
-
return run;
|
|
3892
|
-
}
|
|
3893
|
-
return {
|
|
3894
|
-
apply(mutation, options) {
|
|
3895
|
-
return serialize(async () => {
|
|
3896
|
-
let op;
|
|
3897
|
-
try {
|
|
3898
|
-
op = await buildRouteOperation(store, mutation);
|
|
3899
|
-
} catch (error) {
|
|
3900
|
-
if (error instanceof RouteMutationError) {
|
|
3901
|
-
return { ok: false, code: error.code, message: error.message };
|
|
3902
|
-
}
|
|
3903
|
-
throw error;
|
|
3904
|
-
}
|
|
3905
|
-
if (options?.scope && !operationMatchesScopes(op, options.scope)) {
|
|
3906
|
-
return {
|
|
3907
|
-
ok: false,
|
|
3908
|
-
code: "SCOPE_VIOLATION",
|
|
3909
|
-
message: `Mutation on "${mutation.collection}" is outside the provided scope.`
|
|
3910
|
-
};
|
|
3911
|
-
}
|
|
3912
|
-
const result = await server.applyLocalOperation(op);
|
|
3913
|
-
if (result.result !== "applied") {
|
|
3914
|
-
return {
|
|
3915
|
-
ok: false,
|
|
3916
|
-
code: result.rejection?.code ?? "NOT_APPLIED",
|
|
3917
|
-
message: result.rejection?.message ?? `Operation on "${mutation.collection}" was not applied (${result.result}).`
|
|
3918
|
-
};
|
|
3919
|
-
}
|
|
3920
|
-
const record = await store.findRecord(mutation.collection, op.recordId);
|
|
3921
|
-
return { ok: true, operation: op, record };
|
|
3922
|
-
});
|
|
3923
|
-
},
|
|
3924
|
-
async query(collection, options) {
|
|
3925
|
-
const { scope, ...queryOptions } = options ?? {};
|
|
3926
|
-
const records = await store.queryCollection(collection, queryOptions);
|
|
3927
|
-
if (!scope) {
|
|
3928
|
-
return records;
|
|
3929
|
-
}
|
|
3930
|
-
return records.filter((record) => recordMatchesScope(collection, record, scope));
|
|
3931
|
-
},
|
|
3932
|
-
async findById(collection, id, options) {
|
|
3933
|
-
const record = await store.findRecord(collection, id);
|
|
3934
|
-
if (!record) {
|
|
3935
|
-
return null;
|
|
3936
|
-
}
|
|
3937
|
-
if (options?.scope && !recordMatchesScope(collection, record, options.scope)) {
|
|
3938
|
-
return null;
|
|
3939
|
-
}
|
|
3940
|
-
return record;
|
|
3941
|
-
}
|
|
3942
|
-
};
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
// src/server/production-server.ts
|
|
3946
4196
|
var MIME_TYPES = {
|
|
3947
4197
|
".html": "text/html",
|
|
3948
4198
|
".js": "text/javascript",
|
|
@@ -3966,7 +4216,7 @@ function createProductionServer(config) {
|
|
|
3966
4216
|
enableDashboard: true,
|
|
3967
4217
|
...config.syncOptions
|
|
3968
4218
|
});
|
|
3969
|
-
const routeContext =
|
|
4219
|
+
const routeContext = syncServer.getKoraContext();
|
|
3970
4220
|
let httpServer = null;
|
|
3971
4221
|
function getOperationalToken(kind) {
|
|
3972
4222
|
const auth = config.operationalAuth;
|
|
@@ -4106,6 +4356,10 @@ function createProductionServer(config) {
|
|
|
4106
4356
|
res.end(JSON.stringify(result.body ?? null));
|
|
4107
4357
|
}
|
|
4108
4358
|
return {
|
|
4359
|
+
kora: routeContext,
|
|
4360
|
+
getLiveBlobRefs() {
|
|
4361
|
+
return syncServer.getLiveBlobRefs();
|
|
4362
|
+
},
|
|
4109
4363
|
async start() {
|
|
4110
4364
|
const { createServer } = await import("http");
|
|
4111
4365
|
const { createReadStream, existsSync, statSync } = await import("fs");
|
|
@@ -4405,6 +4659,7 @@ function formatUptime(ms) {
|
|
|
4405
4659
|
MixedAuthProvider,
|
|
4406
4660
|
NoAuthProvider,
|
|
4407
4661
|
PostgresServerStore,
|
|
4662
|
+
RETRIABLE_REJECTION_CODES,
|
|
4408
4663
|
SqliteServerStore,
|
|
4409
4664
|
TokenAuthProvider,
|
|
4410
4665
|
WsServerTransport,
|
|
@@ -4415,6 +4670,7 @@ function formatUptime(ms) {
|
|
|
4415
4670
|
createPrettyLogger,
|
|
4416
4671
|
createProductionServer,
|
|
4417
4672
|
createSilentLogger,
|
|
4418
|
-
createSqliteServerStore
|
|
4673
|
+
createSqliteServerStore,
|
|
4674
|
+
isRetriableRejection
|
|
4419
4675
|
});
|
|
4420
4676
|
//# sourceMappingURL=index.cjs.map
|