@happyvertical/smrt-core 0.49.3 → 0.49.5
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 +20 -0
- package/agents/change-feed.md +19 -0
- package/agents/collection-reads.md +21 -0
- package/agents/generators.md +5 -0
- package/agents/memory.md +23 -3
- package/agents/null-equal-indexes.md +88 -0
- package/agents/object-runtime.md +85 -0
- package/agents/schema-paths.md +7 -7
- package/dist/browser.js +2 -2
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +13 -10
- package/dist/cascade.js.map +1 -1
- package/dist/change-feed.d.ts +22 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +147 -6
- package/dist/change-feed.js.map +1 -1
- package/dist/class.js +1 -1
- package/dist/collection-cache.d.ts +10 -1
- package/dist/collection-cache.d.ts.map +1 -1
- package/dist/collection-cache.js +65 -10
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +69 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +142 -9
- package/dist/collection.js.map +1 -1
- package/dist/consumer-plugin/artifact-publication.d.ts +27 -0
- package/dist/consumer-plugin/artifact-publication.d.ts.map +1 -0
- package/dist/consumer-plugin/artifact-publication.js +80 -0
- package/dist/consumer-plugin/artifact-publication.js.map +1 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +44 -6
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/db-errors.js +1 -1
- package/dist/db-errors.js.map +1 -1
- package/dist/embedded-write-queue.d.ts +1 -1
- package/dist/embedded-write-queue.d.ts.map +1 -1
- package/dist/embedded-write-queue.js +16 -6
- package/dist/embedded-write-queue.js.map +1 -1
- package/dist/embeddings/storage.d.ts +16 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +40 -0
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/interceptors.d.ts +22 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +12 -0
- package/dist/interceptors.js.map +1 -1
- package/dist/junction.d.ts +12 -3
- package/dist/junction.d.ts.map +1 -1
- package/dist/junction.js +34 -4
- package/dist/junction.js.map +1 -1
- package/dist/knowledge-config.d.ts +6 -0
- package/dist/knowledge-config.d.ts.map +1 -0
- package/dist/knowledge-config.js +48 -0
- package/dist/knowledge-config.js.map +1 -0
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +91 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +104 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +2 -0
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +2 -0
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations/null-equal-indexes.d.ts +37 -0
- package/dist/migrations/null-equal-indexes.d.ts.map +1 -0
- package/dist/migrations/null-equal-indexes.js +133 -0
- package/dist/migrations/null-equal-indexes.js.map +1 -0
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +6 -0
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations.js +2 -1
- package/dist/object.d.ts +33 -0
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +316 -46
- package/dist/object.js.map +1 -1
- package/dist/plain-json.d.ts +6 -0
- package/dist/plain-json.d.ts.map +1 -0
- package/dist/plain-json.js +41 -0
- package/dist/plain-json.js.map +1 -0
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +6 -2
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +3 -3
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +4 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +4 -3
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/validator.d.ts +1 -0
- package/dist/registry/validator.d.ts.map +1 -1
- package/dist/registry/validator.js +8 -1
- package/dist/registry/validator.js.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +21 -0
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +2 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +8 -2
- package/dist/schema/conflict-target.d.ts.map +1 -1
- package/dist/schema/conflict-target.js +11 -3
- package/dist/schema/conflict-target.js.map +1 -1
- package/dist/schema/ddl/null-equal-index.d.ts +5 -0
- package/dist/schema/ddl/null-equal-index.d.ts.map +1 -0
- package/dist/schema/ddl/null-equal-index.js +17 -0
- package/dist/schema/ddl/null-equal-index.js.map +1 -0
- package/dist/schema/ddl/postgres-strategy.d.ts +2 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +7 -0
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +8 -3
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/manifest-schema.d.ts +2 -1
- package/dist/schema/manifest-schema.d.ts.map +1 -1
- package/dist/schema/manifest-schema.js +12 -4
- package/dist/schema/manifest-schema.js.map +1 -1
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +1 -1
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +2 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/scripts/browser-plain-json-regression.mjs +85 -0
- package/dist/smrt-knowledge.json +73 -15
- package/dist/system/schema.d.ts +14 -3
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +120 -2
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +9 -7
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +24 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +48 -115
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +2 -2
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +18 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +55 -2
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -4
package/dist/object.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { classifyDatabaseError, classifyDialectMessage } from "./db-errors.js";
|
|
2
2
|
import { DatabaseError, ErrorUtils, RuntimeError, SmrtError, TenantIsolationError, ValidationError } from "./errors.js";
|
|
3
3
|
import { toSnakeCase } from "./utils/naming.js";
|
|
4
|
-
import { runCascadeDelete } from "./cascade.js";
|
|
4
|
+
import { buildCascadePlan, cascadeReferencesTo, runCascadeDelete } from "./cascade.js";
|
|
5
5
|
import { broadcastCacheInvalidation, hasCrossProcessCacheInterest, invalidateCollectionCache, resolveDbCacheKey } from "./collection-cache.js";
|
|
6
6
|
import { isEmbeddedDatabase, isPostgresDatabase, usesEmbeddedRevisionFallback, withEmbeddedWriteQueue, withEmbeddedWriteTransaction } from "./embedded-write-queue.js";
|
|
7
7
|
import { GlobalInterceptors, createInterceptorContext, resolveGetStringFilter } from "./interceptors.js";
|
|
@@ -11,6 +11,8 @@ import { SmrtClass } from "./class.js";
|
|
|
11
11
|
import { EmbeddingProvider } from "./embeddings/provider.js";
|
|
12
12
|
import { EmbeddingStorage } from "./embeddings/storage.js";
|
|
13
13
|
import { ContentHasher } from "./embeddings/hash.js";
|
|
14
|
+
import { getBoxedPrimitiveKind, isRawJSON } from "./plain-json.js";
|
|
15
|
+
import { isBatchSafeValidator } from "./registry/validator.js";
|
|
14
16
|
import { resolveOneToManyInverse, resolveRelationshipTargetName } from "./relationship-loader.js";
|
|
15
17
|
import { postgresRevisionCondition } from "./revision-guard.js";
|
|
16
18
|
import { verifyPersistenceTable } from "./schema/table-verifier.js";
|
|
@@ -21,9 +23,76 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
21
23
|
import { buildWhere } from "@happyvertical/sql";
|
|
22
24
|
//#region src/object.ts
|
|
23
25
|
var logger = createLogger({ level: process.env.DEBUG_STI ? "debug" : "info" });
|
|
26
|
+
var nativeJunctionClients = /* @__PURE__ */ new WeakSet();
|
|
24
27
|
function isDuckDbHugeInt(value) {
|
|
25
28
|
return Boolean(value && typeof value === "object" && "hugeint" in value && (typeof value.hugeint === "number" || typeof value.hugeint === "bigint"));
|
|
26
29
|
}
|
|
30
|
+
var PLAIN_JSON_OMITTED = Symbol("plain-json-omitted");
|
|
31
|
+
/**
|
|
32
|
+
* Materialize the values JSON.stringify() would emit without first encoding
|
|
33
|
+
* them into a string. This keeps toPlainObject() suitable for SvelteKit while
|
|
34
|
+
* avoiding an encode/decode round trip for every model in a collection.
|
|
35
|
+
*/
|
|
36
|
+
function toPlainJSONValue(value, key, ancestors, applyToJSON = true) {
|
|
37
|
+
if (value === null) return null;
|
|
38
|
+
let valueType = typeof value;
|
|
39
|
+
if (applyToJSON && (valueType === "object" || valueType === "function" || valueType === "bigint")) {
|
|
40
|
+
const toJSON = value.toJSON;
|
|
41
|
+
if (typeof toJSON === "function") {
|
|
42
|
+
value = Reflect.apply(toJSON, value, [key]);
|
|
43
|
+
if (value === null) return null;
|
|
44
|
+
valueType = typeof value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
switch (valueType) {
|
|
48
|
+
case "string":
|
|
49
|
+
case "boolean": return value;
|
|
50
|
+
case "number": return Number.isFinite(value) ? value === 0 ? 0 : value : null;
|
|
51
|
+
case "undefined":
|
|
52
|
+
case "function":
|
|
53
|
+
case "symbol": return PLAIN_JSON_OMITTED;
|
|
54
|
+
case "bigint": throw new TypeError("Do not know how to serialize a BigInt");
|
|
55
|
+
}
|
|
56
|
+
const objectValue = value;
|
|
57
|
+
const isArray = Array.isArray(objectValue);
|
|
58
|
+
if (!isArray && isRawJSON(objectValue)) return JSON.parse(objectValue.rawJSON);
|
|
59
|
+
switch (isArray ? void 0 : getBoxedPrimitiveKind(objectValue)) {
|
|
60
|
+
case "number": {
|
|
61
|
+
const numberValue = +objectValue;
|
|
62
|
+
return Number.isFinite(numberValue) ? numberValue === 0 ? 0 : numberValue : null;
|
|
63
|
+
}
|
|
64
|
+
case "string": return String(objectValue);
|
|
65
|
+
case "boolean": return Boolean.prototype.valueOf.call(objectValue);
|
|
66
|
+
case "bigint": throw new TypeError("Do not know how to serialize a BigInt");
|
|
67
|
+
}
|
|
68
|
+
if (ancestors.includes(objectValue)) throw new TypeError("Converting circular structure to JSON");
|
|
69
|
+
ancestors.push(objectValue);
|
|
70
|
+
try {
|
|
71
|
+
if (isArray) {
|
|
72
|
+
const result = [];
|
|
73
|
+
const length = Math.min(Math.max(Math.trunc(+objectValue.length) || 0, 0), Number.MAX_SAFE_INTEGER);
|
|
74
|
+
for (let index = 0; index < length; index++) {
|
|
75
|
+
const item = toPlainJSONValue(objectValue[index], String(index), ancestors);
|
|
76
|
+
result.push(item === PLAIN_JSON_OMITTED ? null : item);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
const result = {};
|
|
81
|
+
for (const property of Object.keys(objectValue)) {
|
|
82
|
+
const item = toPlainJSONValue(objectValue[property], property, ancestors);
|
|
83
|
+
if (item !== PLAIN_JSON_OMITTED) if (property === "__proto__") Object.defineProperty(result, property, {
|
|
84
|
+
configurable: true,
|
|
85
|
+
enumerable: true,
|
|
86
|
+
value: item,
|
|
87
|
+
writable: true
|
|
88
|
+
});
|
|
89
|
+
else result[property] = item;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
} finally {
|
|
93
|
+
ancestors.pop();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
27
96
|
/**
|
|
28
97
|
* Default maximum number of characters of serialized object data injected into
|
|
29
98
|
* the AI prompts built by `is()`, `do()`, and `describe()`.
|
|
@@ -780,7 +849,7 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
780
849
|
* ```
|
|
781
850
|
*/
|
|
782
851
|
toPlainObject() {
|
|
783
|
-
return
|
|
852
|
+
return toPlainJSONValue(this.toJSON(), "", [], false);
|
|
784
853
|
}
|
|
785
854
|
/**
|
|
786
855
|
* Collect public-serialization field policy in one registry traversal.
|
|
@@ -1072,6 +1141,233 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1072
1141
|
for (const columnName of uuidColumns) if (data[columnName] === "") data[columnName] = null;
|
|
1073
1142
|
}
|
|
1074
1143
|
/**
|
|
1144
|
+
* Conservative eligibility for junction batching. Additional methods are fine;
|
|
1145
|
+
* replacing any runtime method/accessor requires the sequential lifecycle.
|
|
1146
|
+
* Re-evaluate for every call: registry and interceptor registration is dynamic.
|
|
1147
|
+
* @internal
|
|
1148
|
+
*/
|
|
1149
|
+
supportsJunctionBatch() {
|
|
1150
|
+
if (!SmrtObject.hasBaseJunctionLifecycle(this)) return false;
|
|
1151
|
+
const name = this.getResolvedQualifiedName();
|
|
1152
|
+
if (ObjectRegistry.getTableStrategy(name) === "sti" || this._insertOnly || !GlobalInterceptors.supportsBulkMutation(this.getResolvedClassName()) || buildCascadePlan(ObjectRegistry, name).references.length > 0 || ObjectRegistry.resolveEmbeddingConfig(this.getResolvedClassName())) return false;
|
|
1153
|
+
const config = this.getRegisteredClassInfo()?.config;
|
|
1154
|
+
if (config?.hooks && Object.keys(config.hooks).length > 0) return false;
|
|
1155
|
+
if (ObjectRegistry.getValidationRules(this.getResolvedClassName()) === void 0 && (ObjectRegistry.getValidators(this.getResolvedClassName()) ?? []).some((validator) => !isBatchSafeValidator(validator))) return false;
|
|
1156
|
+
for (const field of ObjectRegistry.getFields(name).values()) if (field.type === "crossPackageRef" && (field._meta || field).validate) return false;
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
/** Check overrides before initialization can execute custom model code. */
|
|
1160
|
+
static hasBaseJunctionLifecycle(candidate) {
|
|
1161
|
+
let prototype = candidate;
|
|
1162
|
+
while (prototype && prototype !== SmrtObject.prototype) {
|
|
1163
|
+
for (const key of Object.getOwnPropertyNames(prototype)) {
|
|
1164
|
+
if (key === "constructor") continue;
|
|
1165
|
+
const own = Object.getOwnPropertyDescriptor(prototype, key);
|
|
1166
|
+
if (own?.get || own?.set) return false;
|
|
1167
|
+
let base = SmrtObject.prototype;
|
|
1168
|
+
while (base) {
|
|
1169
|
+
const original = Object.getOwnPropertyDescriptor(base, key);
|
|
1170
|
+
if (original) {
|
|
1171
|
+
if (typeof original.value === "function" && own?.value !== original.value || original.get && own?.get !== original.get || original.set && own?.set !== original.set) return false;
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
base = Object.getPrototypeOf(base);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
1178
|
+
}
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Persist a bounded compatible junction replacement through owned lifecycle
|
|
1183
|
+
* preparation/completion. False means no persistence occurred; callers retain
|
|
1184
|
+
* their virtual per-row path. Unsupported shapes never use raw SQL shortcuts.
|
|
1185
|
+
* @internal
|
|
1186
|
+
*/
|
|
1187
|
+
static async tryJunctionBatch(removed, added) {
|
|
1188
|
+
const all = [...removed, ...added];
|
|
1189
|
+
const first = all[0];
|
|
1190
|
+
if (!first) return true;
|
|
1191
|
+
if (all.length > 100 || all.some((item) => item.db !== first.db || item.tableName !== first.tableName || item.constructor !== first.constructor || !SmrtObject.prototype.supportsJunctionBatch.call(item)) || added.some((item) => item.isPersisted) || removed.some((item) => !item.id)) return false;
|
|
1192
|
+
const dbCapabilities = first.db;
|
|
1193
|
+
if (first.getDatabaseEngineHint() === "json") return false;
|
|
1194
|
+
const duck = first.isNativeDuckDb();
|
|
1195
|
+
if (duck) {
|
|
1196
|
+
const client = dbCapabilities.client;
|
|
1197
|
+
if (!client) return false;
|
|
1198
|
+
if (typeof dbCapabilities.getTableSchema === "function" || first.getDatabaseEngineHint() === "duckdb") nativeJunctionClients.add(client);
|
|
1199
|
+
if (!nativeJunctionClients.has(client)) return false;
|
|
1200
|
+
}
|
|
1201
|
+
const engine = isPostgresDatabase(first.db) ? "postgres" : duck ? "duckdb" : detectEngine(first.db.url ?? "", first.getDatabaseEngineHint());
|
|
1202
|
+
if (![
|
|
1203
|
+
"sqlite",
|
|
1204
|
+
"duckdb",
|
|
1205
|
+
"postgres"
|
|
1206
|
+
].includes(engine)) return false;
|
|
1207
|
+
const prepared = [];
|
|
1208
|
+
try {
|
|
1209
|
+
for (const item of added) prepared.push(await item.prepareSave(void 0));
|
|
1210
|
+
} catch {
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
const columns = prepared[0] ? Object.keys(prepared[0].data) : [];
|
|
1214
|
+
const conflict = prepared[0]?.conflictColumns ?? [];
|
|
1215
|
+
const identities = /* @__PURE__ */ new Set();
|
|
1216
|
+
if (columns.length * added.length > 900 || prepared.some((entry) => {
|
|
1217
|
+
if (entry.writePlan.type !== "upsert" || JSON.stringify(Object.keys(entry.data)) !== JSON.stringify(columns) || JSON.stringify(entry.conflictColumns) !== JSON.stringify(conflict) || conflict.some((key) => entry.data[key] == null)) return true;
|
|
1218
|
+
const identity = JSON.stringify(conflict.map((key) => entry.data[key]));
|
|
1219
|
+
if (identities.has(identity)) return true;
|
|
1220
|
+
identities.add(identity);
|
|
1221
|
+
return false;
|
|
1222
|
+
})) return false;
|
|
1223
|
+
const deletions = [];
|
|
1224
|
+
for (const instance of removed) {
|
|
1225
|
+
const context = createInterceptorContext(instance.constructor.name, "delete", void 0, void 0, instance.getResolvedQualifiedName());
|
|
1226
|
+
await GlobalInterceptors.executeBeforeDelete(instance, context);
|
|
1227
|
+
await instance.runHook("beforeDelete");
|
|
1228
|
+
await instance.verifyStorageReady();
|
|
1229
|
+
deletions.push({
|
|
1230
|
+
instance,
|
|
1231
|
+
context
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
if (!GlobalInterceptors.supportsBulkMutation(first.getResolvedClassName())) return false;
|
|
1235
|
+
if (removed.length > 0) {
|
|
1236
|
+
const cascade = await withEmbeddedWriteTransaction(first.db, isEmbeddedDatabase(first.db), async (db) => {
|
|
1237
|
+
const ids = removed.map((item) => item.id);
|
|
1238
|
+
const result = await cascadeReferencesTo(db, ObjectRegistry, {
|
|
1239
|
+
className: first.getResolvedQualifiedName(),
|
|
1240
|
+
tableName: first.tableName,
|
|
1241
|
+
ids
|
|
1242
|
+
});
|
|
1243
|
+
await db.delete(first.tableName, { "id in": ids });
|
|
1244
|
+
return result;
|
|
1245
|
+
}, true);
|
|
1246
|
+
for (const instance of removed) {
|
|
1247
|
+
instance._persisted = false;
|
|
1248
|
+
instance.invalidateCollectionReadCache();
|
|
1249
|
+
await instance.runHook("afterDelete");
|
|
1250
|
+
}
|
|
1251
|
+
for (const table of cascade.affectedTables) first.invalidateCollectionReadCache(table, cascade.affectedTableClasses.get(table));
|
|
1252
|
+
await GlobalInterceptors.executeBulkAfter("afterDelete", deletions);
|
|
1253
|
+
}
|
|
1254
|
+
if (added.length > 0) {
|
|
1255
|
+
const quote = (identifier) => `"${identifier.replaceAll("\"", "\"\"")}"`;
|
|
1256
|
+
const values = [];
|
|
1257
|
+
const tuples = prepared.map(({ data }) => `(${columns.map((column) => {
|
|
1258
|
+
const value = data[column];
|
|
1259
|
+
values.push(value instanceof Date ? value.toISOString() : value === void 0 ? null : value !== null && typeof value === "object" ? JSON.stringify(value) : typeof value === "boolean" && engine === "sqlite" ? Number(value) : value);
|
|
1260
|
+
return engine === "postgres" ? `$${values.length}` : "?";
|
|
1261
|
+
}).join(", ")})`);
|
|
1262
|
+
const updates = columns.map((column) => `${quote(column)} = excluded.${quote(column)}`).join(", ");
|
|
1263
|
+
const sql = `INSERT INTO ${quote(first.tableName)} (${columns.map(quote).join(", ")}) VALUES ${tuples.join(", ")} ON CONFLICT (${conflict.map(quote).join(", ")}) DO UPDATE SET ${updates}`;
|
|
1264
|
+
await withEmbeddedWriteQueue(first.db, isEmbeddedDatabase(first.db), () => ErrorUtils.withRetry(async () => {
|
|
1265
|
+
try {
|
|
1266
|
+
await first.db.query(sql, ...values);
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (!(error instanceof Error)) throw error;
|
|
1269
|
+
const classification = classifyDatabaseError(error);
|
|
1270
|
+
const field = first.extractConstraintFieldFromChain(error, classification);
|
|
1271
|
+
if (classification.kind === "unique_violation") throw ValidationError.uniqueConstraint(field, first.getFieldValue(field));
|
|
1272
|
+
if (classification.kind === "not_null_violation") throw ValidationError.requiredField(field, first.getResolvedClassName());
|
|
1273
|
+
throw DatabaseError.queryFailed(`UPSERT INTO ${first.tableName}`, error);
|
|
1274
|
+
}
|
|
1275
|
+
}, 3, 500));
|
|
1276
|
+
const completions = [];
|
|
1277
|
+
for (let i = 0; i < added.length; i++) {
|
|
1278
|
+
await added[i].completeSave(prepared[i].interceptorContext, false);
|
|
1279
|
+
completions.push({
|
|
1280
|
+
instance: added[i],
|
|
1281
|
+
context: prepared[i].interceptorContext
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
await GlobalInterceptors.executeBulkAfter("afterSave", completions);
|
|
1285
|
+
}
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
/** Shared preparation for ordinary saves and compatible bulk creates. */
|
|
1289
|
+
async prepareSave(revisionGuard) {
|
|
1290
|
+
const className = this.getResolvedClassName();
|
|
1291
|
+
await this.validateBeforeSave();
|
|
1292
|
+
await this.validateCrossPackageRefs();
|
|
1293
|
+
const interceptorContext = createInterceptorContext(className, "save", void 0, void 0, this.getResolvedQualifiedName());
|
|
1294
|
+
await GlobalInterceptors.executeBeforeSave(this, interceptorContext);
|
|
1295
|
+
if (!this.id) this.id = crypto.randomUUID();
|
|
1296
|
+
if (!this.slug) this.slug = await this.getSlug();
|
|
1297
|
+
this.updated_at = this.nextRevisionTimestamp(revisionGuard);
|
|
1298
|
+
if (!this.created_at) this.created_at = /* @__PURE__ */ new Date();
|
|
1299
|
+
await this.verifyStorageReady();
|
|
1300
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(this.getResolvedQualifiedName());
|
|
1301
|
+
if (process.env.NODE_ENV === "development") {
|
|
1302
|
+
if (this.toJSON !== SmrtObject.prototype.toJSON && tableStrategy === "sti") logger.warn(`[SMRT STI Warning] ${this.constructor.name} overrides toJSON() but uses STI.\nEnsure super.toJSON() is called or _meta_type is set manually.\nThis can cause "Missing _meta_type discriminator" errors.\nPrefer using the transformJSON() hook instead of overriding toJSON().\nSee issue #377: https://github.com/happyvertical/smrt/issues/377`);
|
|
1303
|
+
}
|
|
1304
|
+
if (tableStrategy === "sti") {
|
|
1305
|
+
warnIfSkipRehydrateSet();
|
|
1306
|
+
const qualifiedName = this.getResolvedQualifiedName();
|
|
1307
|
+
const classesNeedingFreshSTIFieldState = Array.from(/* @__PURE__ */ new Set([qualifiedName, ...getSTIHierarchyMembers(qualifiedName)]));
|
|
1308
|
+
for (const stiClassName of classesNeedingFreshSTIFieldState) await ObjectRegistry.getAllFields(stiClassName);
|
|
1309
|
+
}
|
|
1310
|
+
const jsonData = this.toJSON();
|
|
1311
|
+
if (tableStrategy === "sti") {
|
|
1312
|
+
if (!jsonData._meta_type) throw new Error(`STI validation failed: Missing _meta_type discriminator when saving ${className}. This should have been set automatically by toJSON(). Please report this bug.`);
|
|
1313
|
+
if (!isValidMetaType(jsonData._meta_type, className)) throw new Error(`STI validation failed: _meta_type mismatch when saving ${className}. Expected '${getExpectedMetaType(className)}' but got '${jsonData._meta_type}'. This should not happen - please report this bug.`);
|
|
1314
|
+
}
|
|
1315
|
+
const data = {};
|
|
1316
|
+
for (const [key, value] of Object.entries(jsonData)) if (key.startsWith("_")) data[key] = value;
|
|
1317
|
+
else data[toSnakeCase(key)] = value;
|
|
1318
|
+
data.updated_at = this.updated_at;
|
|
1319
|
+
await this.coerceEmptyUuidValuesToNull(className, data);
|
|
1320
|
+
const conflictColumns = ObjectRegistry.getConflictColumns(className);
|
|
1321
|
+
const derivedColumns = this.getPersistenceDerivedColumns();
|
|
1322
|
+
const registeredFields = ObjectRegistry.getFields(className);
|
|
1323
|
+
const registeredColumns = new Set([...registeredFields].filter(([, field]) => field.type !== "oneToMany" && field.type !== "manyToMany").map(([name]) => toSnakeCase(name)));
|
|
1324
|
+
const protectedColumns = /* @__PURE__ */ new Set([
|
|
1325
|
+
"id",
|
|
1326
|
+
"slug",
|
|
1327
|
+
"tenant_id",
|
|
1328
|
+
"created_at",
|
|
1329
|
+
"updated_at",
|
|
1330
|
+
"_meta_type",
|
|
1331
|
+
"_meta_data",
|
|
1332
|
+
...conflictColumns.map(toSnakeCase)
|
|
1333
|
+
]);
|
|
1334
|
+
for (const [name, field] of registeredFields) if (field.__tenancy?.isTenantIdField || field._meta?.__tenancy?.isTenantIdField) protectedColumns.add(toSnakeCase(name));
|
|
1335
|
+
for (const column of derivedColumns) if (!registeredColumns.has(column) || protectedColumns.has(column)) throw RuntimeError.invalidState(`Invalid persistence derived column declaration: ${column}`, {
|
|
1336
|
+
className,
|
|
1337
|
+
column
|
|
1338
|
+
});
|
|
1339
|
+
const normalizedColumns = this.normalizePersistenceData(Object.freeze({ ...data }));
|
|
1340
|
+
if (normalizedColumns) {
|
|
1341
|
+
for (const column of Object.keys(normalizedColumns)) if (!derivedColumns.includes(column)) throw RuntimeError.invalidState(`Undeclared persistence derived column: ${column}`, {
|
|
1342
|
+
className,
|
|
1343
|
+
column
|
|
1344
|
+
});
|
|
1345
|
+
Object.assign(data, normalizedColumns);
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
interceptorContext,
|
|
1349
|
+
data,
|
|
1350
|
+
writePlan: await this.planPersistenceWrite(className, tableStrategy, data, conflictColumns),
|
|
1351
|
+
conflictColumns
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
async completeSave(interceptorContext, runInterceptors = true) {
|
|
1355
|
+
const className = this.getResolvedClassName();
|
|
1356
|
+
this._persisted = true;
|
|
1357
|
+
this.invalidateCollectionReadCache();
|
|
1358
|
+
if (runInterceptors) await GlobalInterceptors.executeAfterSave(this, interceptorContext);
|
|
1359
|
+
const embeddingConfig = ObjectRegistry.resolveEmbeddingConfig(className);
|
|
1360
|
+
const skipAutoEmbeddings = this.options._skipAutoEmbeddings === true;
|
|
1361
|
+
if (embeddingConfig && embeddingConfig.autoGenerate !== false && !skipAutoEmbeddings) {
|
|
1362
|
+
if (await this.getOptionalAiClient()) {
|
|
1363
|
+
if (await this.hasStaleEmbeddings()) this.generateEmbeddings().catch((error) => {
|
|
1364
|
+
logger.warn(`Failed to auto-generate embeddings for ${this.constructor.name}`, { error: error instanceof Error ? error.message : error });
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (skipAutoEmbeddings) this.options._skipAutoEmbeddings = false;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1075
1371
|
* Persists this object to the database using an upsert (insert or update).
|
|
1076
1372
|
*
|
|
1077
1373
|
* Steps performed on every save:
|
|
@@ -1131,37 +1427,7 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1131
1427
|
const revisionGuard = loadedRevision instanceof Date ? loadedRevision.toISOString() : loadedRevision;
|
|
1132
1428
|
try {
|
|
1133
1429
|
if (expectedUpdatedAt !== void 0 && (!this._persisted || !this.id)) throw RuntimeError.invalidState("A revision-guarded save requires a persisted object", { className });
|
|
1134
|
-
await this.
|
|
1135
|
-
await this.validateCrossPackageRefs();
|
|
1136
|
-
const interceptorContext = createInterceptorContext(className, "save", void 0, void 0, this.getResolvedQualifiedName());
|
|
1137
|
-
await GlobalInterceptors.executeBeforeSave(this, interceptorContext);
|
|
1138
|
-
if (!this.id) this.id = crypto.randomUUID();
|
|
1139
|
-
if (!this.slug) this.slug = await this.getSlug();
|
|
1140
|
-
this.updated_at = this.nextRevisionTimestamp(revisionGuard);
|
|
1141
|
-
if (!this.created_at) this.created_at = /* @__PURE__ */ new Date();
|
|
1142
|
-
await this.verifyStorageReady();
|
|
1143
|
-
const tableStrategy = ObjectRegistry.getTableStrategy(this.getResolvedQualifiedName());
|
|
1144
|
-
if (process.env.NODE_ENV === "development") {
|
|
1145
|
-
if (this.toJSON !== SmrtObject.prototype.toJSON && tableStrategy === "sti") logger.warn(`[SMRT STI Warning] ${this.constructor.name} overrides toJSON() but uses STI.\nEnsure super.toJSON() is called or _meta_type is set manually.\nThis can cause "Missing _meta_type discriminator" errors.\nPrefer using the transformJSON() hook instead of overriding toJSON().\nSee issue #377: https://github.com/happyvertical/smrt/issues/377`);
|
|
1146
|
-
}
|
|
1147
|
-
if (tableStrategy === "sti") {
|
|
1148
|
-
warnIfSkipRehydrateSet();
|
|
1149
|
-
const qualifiedName = this.getResolvedQualifiedName();
|
|
1150
|
-
const classesNeedingFreshSTIFieldState = Array.from(/* @__PURE__ */ new Set([qualifiedName, ...getSTIHierarchyMembers(qualifiedName)]));
|
|
1151
|
-
for (const stiClassName of classesNeedingFreshSTIFieldState) await ObjectRegistry.getAllFields(stiClassName);
|
|
1152
|
-
}
|
|
1153
|
-
const jsonData = this.toJSON();
|
|
1154
|
-
if (tableStrategy === "sti") {
|
|
1155
|
-
if (!jsonData._meta_type) throw new Error(`STI validation failed: Missing _meta_type discriminator when saving ${className}. This should have been set automatically by toJSON(). Please report this bug.`);
|
|
1156
|
-
if (!isValidMetaType(jsonData._meta_type, className)) throw new Error(`STI validation failed: _meta_type mismatch when saving ${className}. Expected '${getExpectedMetaType(className)}' but got '${jsonData._meta_type}'. This should not happen - please report this bug.`);
|
|
1157
|
-
}
|
|
1158
|
-
const data = {};
|
|
1159
|
-
for (const [key, value] of Object.entries(jsonData)) if (key.startsWith("_")) data[key] = value;
|
|
1160
|
-
else data[toSnakeCase(key)] = value;
|
|
1161
|
-
data.updated_at = this.updated_at;
|
|
1162
|
-
await this.coerceEmptyUuidValuesToNull(className, data);
|
|
1163
|
-
const conflictColumns = ObjectRegistry.getConflictColumns(className);
|
|
1164
|
-
const writePlan = await this.planPersistenceWrite(className, tableStrategy, data, conflictColumns);
|
|
1430
|
+
const { interceptorContext, data, writePlan, conflictColumns } = await this.prepareSave(revisionGuard);
|
|
1165
1431
|
const upsertConflictColumns = this._persisted && data.id ? ["id"] : conflictColumns;
|
|
1166
1432
|
const useEmbeddedRevisionFallback = revisionGuard !== void 0 && usesEmbeddedRevisionFallback(this.db);
|
|
1167
1433
|
const serializeEmbeddedWrite = isEmbeddedDatabase(this.db);
|
|
@@ -1210,19 +1476,7 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1210
1476
|
id: this.id
|
|
1211
1477
|
});
|
|
1212
1478
|
revisionPersisted = true;
|
|
1213
|
-
this.
|
|
1214
|
-
this.invalidateCollectionReadCache();
|
|
1215
|
-
await GlobalInterceptors.executeAfterSave(this, interceptorContext);
|
|
1216
|
-
const embeddingConfig = ObjectRegistry.resolveEmbeddingConfig(className);
|
|
1217
|
-
const skipAutoEmbeddings = this.options._skipAutoEmbeddings === true;
|
|
1218
|
-
if (embeddingConfig && embeddingConfig.autoGenerate !== false && !skipAutoEmbeddings) {
|
|
1219
|
-
if (await this.getOptionalAiClient()) {
|
|
1220
|
-
if (await this.hasStaleEmbeddings()) this.generateEmbeddings().catch((error) => {
|
|
1221
|
-
logger.warn(`Failed to auto-generate embeddings for ${this.constructor.name}`, { error: error instanceof Error ? error.message : error });
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
if (skipAutoEmbeddings) this.options._skipAutoEmbeddings = false;
|
|
1479
|
+
await this.completeSave(interceptorContext);
|
|
1226
1480
|
return this;
|
|
1227
1481
|
} catch (error) {
|
|
1228
1482
|
if (!revisionPersisted) this.updated_at = previousUpdatedAt;
|
|
@@ -1346,6 +1600,22 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1346
1600
|
const name = candidate?.name;
|
|
1347
1601
|
return name === "TenantIsolationError" || name === "TenantContextError";
|
|
1348
1602
|
}
|
|
1603
|
+
/** Declare permitted snake-case derived schema columns; preserve super entries. */
|
|
1604
|
+
getPersistenceDerivedColumns() {
|
|
1605
|
+
return [];
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Return derived schema-column values for the final persistence row.
|
|
1609
|
+
* Runs synchronously after beforeSave, the complete toJSON/transformJSON
|
|
1610
|
+
* chain, snake-case mapping and UUID coercion; returned columns are merged
|
|
1611
|
+
* before every save branch. This does not change public serialization.
|
|
1612
|
+
* Only derive columns from this read-only snapshot: do not modify source,
|
|
1613
|
+
* identity, tenant or revision columns, and do not perform I/O. Overrides
|
|
1614
|
+
* should preserve any columns returned by super. Declare permitted snake-case
|
|
1615
|
+
* schema columns in getPersistenceDerivedColumns(), preserving super entries.
|
|
1616
|
+
* Framework identity, tenant, revision and conflict columns cannot be declared.
|
|
1617
|
+
*/
|
|
1618
|
+
normalizePersistenceData(_data) {}
|
|
1349
1619
|
/**
|
|
1350
1620
|
* Validates object state before saving
|
|
1351
1621
|
* Override in subclasses to add custom validation logic
|