bazis 0.98.15 → 0.98.17
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/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/scripts/orm-predicate-codegen.ts +6 -2
- package/library/orm/Metadata/decorators.ts +2 -2
- package/library/orm/Query/EntityQuery.ts +18 -5
- package/library/orm/Query/ImmediateMutations.ts +1 -1
- package/library/orm/Query/conditions.ts +39 -13
- package/library/orm/Saving/SaveExecutor.ts +11 -1
- package/library/orm/Saving/navigationFixup.ts +80 -0
- package/library/orm/Tracking/ChangeTracker.ts +5 -0
- package/package.json +1 -1
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.17" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import ts from "typescript";
|
|
2
3
|
|
|
3
4
|
/** Reject JS truthiness where the caller is constructing an ORM condition. */
|
|
@@ -11,8 +12,11 @@ export function analyzeOrmPredicates(checker: ts.TypeChecker, source: ts.SourceF
|
|
|
11
12
|
};
|
|
12
13
|
const reject = (node: ts.Node): void => {
|
|
13
14
|
const location = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
14
|
-
diagnostics
|
|
15
|
-
|
|
15
|
+
// Relative like the other codegen diagnostics: the project path is noise.
|
|
16
|
+
const fileName = (path.isAbsolute(source.fileName) ? path.relative(".", source.fileName) : source.fileName).replaceAll("\\", "/");
|
|
17
|
+
diagnostics.push(`BAZIS_ORM_PREDICATE_LOGIC: ${fileName}:${location.line + 1}:${location.character + 1}: `
|
|
18
|
+
+ "ORM conditions cannot use JavaScript truthiness: JavaScript keeps only one side of && and ||. "
|
|
19
|
+
+ "Use .and(), .or(), .not(): (p) => p.views.gt(70).and(p.authorId.eq(2)).");
|
|
16
20
|
};
|
|
17
21
|
const visit = (node: ts.Node): void => {
|
|
18
22
|
if (ts.isBinaryExpression(node)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ColumnOptionsType, RelationKind, PropertyConvention } from "./types";
|
|
2
2
|
import type { ValueConverter } from "./ValueConverter";
|
|
3
|
-
import {
|
|
3
|
+
import { evaluatePredicate, type PredicateFn } from "../Query/conditions";
|
|
4
4
|
import type { Condition } from "../Query/conditions";
|
|
5
5
|
import { compileCheck, type CheckPredicate } from "../Schema/CheckExpression";
|
|
6
6
|
|
|
@@ -371,7 +371,7 @@ export function HasConversion(converter: ValueConverter) {
|
|
|
371
371
|
export function QueryFilter<T extends object>(predicate: PredicateFn<T>) {
|
|
372
372
|
return (_value: abstract new (...args: never[]) => unknown, context: ClassDecoratorContext): void => {
|
|
373
373
|
const raw = ownRaw(context.metadata);
|
|
374
|
-
const node = predicate
|
|
374
|
+
const node = evaluatePredicate(predicate, "@QueryFilter").node;
|
|
375
375
|
raw.queryFilters = [...(raw.queryFilters ?? []), node];
|
|
376
376
|
};
|
|
377
377
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EntityNotFoundError } from "../errors";
|
|
2
2
|
import type { EntityModel } from "../Metadata/types";
|
|
3
3
|
import type { DbContextRuntime } from "../runtime";
|
|
4
|
-
import { fieldSelector, type KeySelectorFn, type PredicateFn } from "./conditions";
|
|
4
|
+
import { evaluatePredicate, fieldSelector, type KeySelectorFn, type PredicateFn } from "./conditions";
|
|
5
5
|
import { IncludeLoader } from "./IncludeLoader";
|
|
6
6
|
import { materialize, materializeProjection } from "./materialize";
|
|
7
7
|
import { EMPTY_PLAN, withCondition, withOrder, type QueryPlan } from "./QueryPlan";
|
|
@@ -30,6 +30,16 @@ function captureNavigation<T>(selector: (entity: T) => unknown): string {
|
|
|
30
30
|
return captured;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const SELECT_HINT = "select() maps properties as they are, e.g. (p) => ({ name: p.title }); compute values after toList().";
|
|
34
|
+
|
|
35
|
+
/** A property read inside `select`; turning it into a string or number is a computation select cannot translate. */
|
|
36
|
+
class ProjectedField {
|
|
37
|
+
constructor(readonly property: string) {}
|
|
38
|
+
[Symbol.toPrimitive](): never {
|
|
39
|
+
throw new TypeError(SELECT_HINT);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
/** Captures the `.select(u => ({ alias: u.prop }))` projection. */
|
|
34
44
|
function captureProjection<T, R extends Record<string, unknown>>(
|
|
35
45
|
selector: (entity: T) => R,
|
|
@@ -37,13 +47,16 @@ function captureProjection<T, R extends Record<string, unknown>>(
|
|
|
37
47
|
const proxy = new Proxy(
|
|
38
48
|
{},
|
|
39
49
|
{
|
|
40
|
-
get(_target, property):
|
|
41
|
-
return String(property);
|
|
50
|
+
get(_target, property): ProjectedField {
|
|
51
|
+
return new ProjectedField(String(property));
|
|
42
52
|
},
|
|
43
53
|
},
|
|
44
54
|
);
|
|
45
55
|
const mapped = selector(proxy as T);
|
|
46
|
-
return Object.entries(mapped).map(([alias,
|
|
56
|
+
return Object.entries(mapped).map(([alias, field]) => {
|
|
57
|
+
if (!(field instanceof ProjectedField)) throw new TypeError(`${SELECT_HINT} "${alias}" is not a property.`);
|
|
58
|
+
return { alias, property: field.property };
|
|
59
|
+
});
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
/**
|
|
@@ -64,7 +77,7 @@ export class EntityQuery<T extends object, TResult = T> {
|
|
|
64
77
|
|
|
65
78
|
/** Filter. Several calls are combined with AND. */
|
|
66
79
|
where(predicate: PredicateFn<T>): EntityQuery<T, TResult> {
|
|
67
|
-
const result = predicate
|
|
80
|
+
const result = evaluatePredicate(predicate, "where");
|
|
68
81
|
return this.derive(withCondition(this.plan, result.node));
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -143,7 +143,7 @@ function cloneCondition(model: EntityModel, input: unknown, active: Set<object>,
|
|
|
143
143
|
const exact = (...keys: string[]) => { if (fields.size !== keys.length || keys.some((key) => !fields.has(key))) unsafe(); };
|
|
144
144
|
const property = () => { const value = fields.get("property")?.value; if (typeof value !== "string" || !model.propertyByName(value)) unsafe(); return value; };
|
|
145
145
|
let result: Condition;
|
|
146
|
-
if (kind === "compare") { const escaped = fields.get("escaped")?.value; if (escaped !== undefined && escaped !== true) unsafe(); exact(...(escaped === undefined ? ["kind", "property", "op", "value"] : ["kind", "property", "op", "value", "escaped"])); const op = fields.get("op")!.value; if (typeof op !== "string" || !["=", "<>", ">", ">=", "<", "<=", "LIKE"].includes(op) || (escaped === true && op !== "LIKE")) unsafe(); result = Object.freeze({ kind: "compare", property: property(), op: op as never, value: snapshot(fields.get("value")!.value, new Set(), new WeakMap(), true), ...(escaped === true ? { escaped: true } : {}) }); }
|
|
146
|
+
if (kind === "compare") { const escaped = fields.get("escaped")?.value; if (escaped !== undefined && escaped !== true) unsafe(); exact(...(escaped === undefined ? ["kind", "property", "op", "value"] : ["kind", "property", "op", "value", "escaped"])); const op = fields.get("op")!.value; if (typeof op !== "string" || !["=", "<>", ">", ">=", "<", "<=", "LIKE", "ILIKE"].includes(op) || (escaped === true && op !== "LIKE" && op !== "ILIKE")) unsafe(); result = Object.freeze({ kind: "compare", property: property(), op: op as never, value: snapshot(fields.get("value")!.value, new Set(), new WeakMap(), true), ...(escaped === true ? { escaped: true } : {}) }); }
|
|
147
147
|
else if (kind === "in") { exact("kind", "property", "values"); result = Object.freeze({ kind: "in", property: property(), values: denseValues(fields.get("values")!.value) }); }
|
|
148
148
|
else if (kind === "tuples") { exact("kind", "properties", "values"); const properties = denseValues(fields.get("properties")!.value); if (properties.some((value) => typeof value !== "string" || !model.propertyByName(value as string))) unsafe(); const rows = denseValues(fields.get("values")!.value).map((row) => { const values = denseValues(row); if (values.length !== properties.length) unsafe(); return Object.freeze(values); }); result = Object.freeze({ kind: "tuples", properties: Object.freeze(properties as string[]), values: Object.freeze(rows) }); }
|
|
149
149
|
else if (kind === "null") { exact("kind", "property", "negated"); const negated = fields.get("negated")!.value; if (typeof negated !== "boolean") unsafe(); result = Object.freeze({ kind: "null", property: property(), negated }); }
|
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
* returns a column operand, comparison methods produce AST nodes, and
|
|
7
7
|
* `.and()/.or()/.not()` combine them. No user strings in the SQL, only parameters.
|
|
8
8
|
*
|
|
9
|
-
* Note: `&&`/`||` are not intercepted; use `.and()/.or()`.
|
|
9
|
+
* Note: `&&`/`||` are not intercepted; use `.and()/.or()`. Codegen rejects
|
|
10
|
+
* them in where/count/any/first/firstOrDefault and @QueryFilter, and a result
|
|
11
|
+
* that is not a Predicate fails with a hint (see {@link evaluatePredicate}).
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
|
-
export type CompareOp = "=" | "<>" | ">" | ">=" | "<" | "<=" | "LIKE";
|
|
14
|
+
export type CompareOp = "=" | "<>" | ">" | ">=" | "<" | "<=" | "LIKE" | "ILIKE";
|
|
15
|
+
|
|
16
|
+
/** Text match options: `ignoreCase` compares case-insensitively (PostgreSQL `ILIKE`). */
|
|
17
|
+
export interface TextMatchOptions {
|
|
18
|
+
readonly ignoreCase?: boolean;
|
|
19
|
+
}
|
|
13
20
|
|
|
14
21
|
export type Condition =
|
|
15
22
|
| {
|
|
@@ -17,7 +24,7 @@ export type Condition =
|
|
|
17
24
|
readonly property: string;
|
|
18
25
|
readonly op: CompareOp;
|
|
19
26
|
readonly value: unknown;
|
|
20
|
-
/** For LIKE from startsWith/endsWith/contains: add `ESCAPE '\'`. */
|
|
27
|
+
/** For LIKE/ILIKE from startsWith/endsWith/contains/eq: add `ESCAPE '\'`. */
|
|
21
28
|
readonly escaped?: boolean;
|
|
22
29
|
}
|
|
23
30
|
| { readonly kind: "in"; readonly property: string; readonly values: readonly unknown[] }
|
|
@@ -49,12 +56,17 @@ export class Operand<T = unknown> {
|
|
|
49
56
|
return new Predicate({ kind: "compare", property: this.property, op, value });
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
/** LIKE with escaped wildcards (needs `ESCAPE '\'` in SQL). */
|
|
53
|
-
private likeEscaped(pattern: string): Predicate {
|
|
54
|
-
return new Predicate({ kind: "compare", property: this.property, op: "LIKE", value: pattern, escaped: true });
|
|
59
|
+
/** LIKE/ILIKE with escaped wildcards (needs `ESCAPE '\'` in SQL). */
|
|
60
|
+
private likeEscaped(pattern: string, options: TextMatchOptions | undefined): Predicate {
|
|
61
|
+
return new Predicate({ kind: "compare", property: this.property, op: options?.ignoreCase === true ? "ILIKE" : "LIKE", value: pattern, escaped: true });
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
/** Equality; `{ ignoreCase: true }` compares strings case-insensitively. */
|
|
65
|
+
eq(value: T, options?: TextMatchOptions): Predicate {
|
|
66
|
+
if (options?.ignoreCase === true) {
|
|
67
|
+
if (typeof value !== "string") throw new TypeError(`eq(..., { ignoreCase: true }) on "${this.property}" needs a string value.`);
|
|
68
|
+
return this.likeEscaped(escapeLike(value), options);
|
|
69
|
+
}
|
|
58
70
|
return value === null ? this.isNull() : this.compare("=", value);
|
|
59
71
|
}
|
|
60
72
|
ne(value: T): Predicate {
|
|
@@ -75,14 +87,14 @@ export class Operand<T = unknown> {
|
|
|
75
87
|
like(pattern: TextValue<T>): Predicate {
|
|
76
88
|
return this.compare("LIKE", pattern);
|
|
77
89
|
}
|
|
78
|
-
startsWith(value: TextValue<T
|
|
79
|
-
return this.likeEscaped(`${escapeLike(value)}
|
|
90
|
+
startsWith(value: TextValue<T>, options?: TextMatchOptions): Predicate {
|
|
91
|
+
return this.likeEscaped(`${escapeLike(value)}%`, options);
|
|
80
92
|
}
|
|
81
|
-
endsWith(value: TextValue<T
|
|
82
|
-
return this.likeEscaped(`%${escapeLike(value)}
|
|
93
|
+
endsWith(value: TextValue<T>, options?: TextMatchOptions): Predicate {
|
|
94
|
+
return this.likeEscaped(`%${escapeLike(value)}`, options);
|
|
83
95
|
}
|
|
84
|
-
contains(value: TextValue<T
|
|
85
|
-
return this.likeEscaped(`%${escapeLike(value)}
|
|
96
|
+
contains(value: TextValue<T>, options?: TextMatchOptions): Predicate {
|
|
97
|
+
return this.likeEscaped(`%${escapeLike(value)}%`, options);
|
|
86
98
|
}
|
|
87
99
|
in(values: readonly T[]): Predicate {
|
|
88
100
|
return new Predicate({ kind: "in", property: this.property, values: [...values] });
|
|
@@ -125,3 +137,17 @@ const FIELD_PROXY: ProxyHandler<object> = {
|
|
|
125
137
|
export function fieldSelector<T>(): FieldSelector<T> {
|
|
126
138
|
return new Proxy({}, FIELD_PROXY) as FieldSelector<T>;
|
|
127
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Runs a predicate function and checks that it built a condition. `>`/`===`
|
|
143
|
+
* on an operand yield a boolean, and `cond && other` with a false left side
|
|
144
|
+
* yields `false`; without this check the query failed deep in SQL building.
|
|
145
|
+
*/
|
|
146
|
+
export function evaluatePredicate<T>(predicate: PredicateFn<T>, method: string): Predicate {
|
|
147
|
+
const result: unknown = predicate(fieldSelector<T>());
|
|
148
|
+
if (result instanceof Predicate) return result;
|
|
149
|
+
throw new TypeError(
|
|
150
|
+
`${method}() expects a condition such as (p) => p.views.gt(70), got ${result === null ? "null" : typeof result}. `
|
|
151
|
+
+ "Use .gt()/.eq()/.and()/.or() instead of >, ===, &&, ||.",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -7,7 +7,8 @@ import type { DatabaseProvider, DbExecutor } from "../Providers/types";
|
|
|
7
7
|
import { isUnknownTransactionOutcome, registerTransactionUncertainty, TransactionOutcomeUnknownError } from "../Providers/transactionOutcome";
|
|
8
8
|
import { registerPostCommitFinalizer } from "../Providers/transactionCallbacks";
|
|
9
9
|
import { EntityState } from "../Tracking/EntityState";
|
|
10
|
-
import { assertUniqueTrackedKeys, holdDeletedIdentity, type ChangeTracker, type TrackedEntry } from "../Tracking/ChangeTracker";
|
|
10
|
+
import { assertUniqueTrackedKeys, holdDeletedIdentity, trackedEntriesOf, type ChangeTracker, type TrackedEntry } from "../Tracking/ChangeTracker";
|
|
11
|
+
import { NavigationFixup } from "./navigationFixup";
|
|
11
12
|
import { CommandBuilder } from "./CommandBuilder";
|
|
12
13
|
import { applyConventions } from "./applyConventions";
|
|
13
14
|
import { ExecutionStrategy, type ExecutionStrategyOptions } from "./ExecutionStrategy";
|
|
@@ -78,6 +79,10 @@ export class SaveExecutor {
|
|
|
78
79
|
|
|
79
80
|
private async saveCore(): Promise<number> {
|
|
80
81
|
if (uncertainTrackers.has(this.tracker)) throw new TransactionOutcomeUnknownError();
|
|
82
|
+
// Foreign keys from navigations first, so a changed navigation on an
|
|
83
|
+
// Unchanged entity is detected as a Modified foreign key.
|
|
84
|
+
const fixup = new NavigationFixup(trackedEntriesOf(this.tracker));
|
|
85
|
+
fixup.applyAll();
|
|
81
86
|
this.tracker.detectChanges();
|
|
82
87
|
const pending = this.tracker.entriesToProcess();
|
|
83
88
|
if (pending.length === 0) {
|
|
@@ -127,9 +132,14 @@ export class SaveExecutor {
|
|
|
127
132
|
group.push(ordered[i]!);
|
|
128
133
|
i += 1;
|
|
129
134
|
}
|
|
135
|
+
// Parents are inserted first: their generated keys are known now.
|
|
136
|
+
for (const added of group) fixup.apply(added);
|
|
130
137
|
await this.insertBatch(tx, entry.model, group);
|
|
131
138
|
continue;
|
|
132
139
|
}
|
|
140
|
+
if (entry.state === EntityState.Modified) {
|
|
141
|
+
for (const name of fixup.apply(entry)) entry.modifiedProperties.add(name);
|
|
142
|
+
}
|
|
133
143
|
await this.execute(tx, entry);
|
|
134
144
|
i += 1;
|
|
135
145
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { EntityState } from "../Tracking/EntityState";
|
|
2
|
+
import type { TrackedEntry } from "../Tracking/ChangeTracker";
|
|
3
|
+
|
|
4
|
+
type Values = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Foreign keys from navigations, as EF Core's relationship fix-up does:
|
|
8
|
+
* `article.author = bob` (a `@ManyToOne`) or `bob.articles.push(article)`
|
|
9
|
+
* (a `@OneToMany`) set `article.authorId` from `bob`'s key. Only navigations to
|
|
10
|
+
* entities tracked by the same context are followed; the navigation wins over
|
|
11
|
+
* a different value in the foreign key property.
|
|
12
|
+
*/
|
|
13
|
+
export class NavigationFixup {
|
|
14
|
+
/** child entity -> the parents whose collection holds it. */
|
|
15
|
+
private readonly parents = new Map<object, { readonly parent: TrackedEntry; readonly foreignKey: readonly string[] }[]>();
|
|
16
|
+
|
|
17
|
+
constructor(private readonly entries: ReadonlyMap<object, TrackedEntry>) {
|
|
18
|
+
for (const entry of entries.values()) {
|
|
19
|
+
for (const relation of entry.model.relations) {
|
|
20
|
+
if (relation.kind !== "collection") continue;
|
|
21
|
+
const children = (entry.entity as Values)[relation.navigationName];
|
|
22
|
+
if (!Array.isArray(children)) continue;
|
|
23
|
+
for (const child of children) {
|
|
24
|
+
if (child === null || typeof child !== "object" || !entries.has(child)) continue;
|
|
25
|
+
const list = this.parents.get(child) ?? [];
|
|
26
|
+
list.push({ parent: entry, foreignKey: toArray(relation.foreignKey) });
|
|
27
|
+
this.parents.set(child, list);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Applies the fix-up to every tracked entry (before change detection). */
|
|
34
|
+
applyAll(): void {
|
|
35
|
+
for (const entry of this.entries.values()) {
|
|
36
|
+
if (entry.state !== EntityState.Deleted) this.apply(entry);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Applies the fix-up to one entry and returns the foreign key properties it
|
|
42
|
+
* changed. Called again right before the entry's SQL, when the keys the
|
|
43
|
+
* database generated for its parents are known.
|
|
44
|
+
*/
|
|
45
|
+
apply(entry: TrackedEntry): readonly string[] {
|
|
46
|
+
const changed: string[] = [];
|
|
47
|
+
const entity = entry.entity as Values;
|
|
48
|
+
for (const relation of entry.model.relations) {
|
|
49
|
+
if (relation.kind !== "reference") continue;
|
|
50
|
+
const target = entity[relation.navigationName];
|
|
51
|
+
if (target === null || typeof target !== "object") continue;
|
|
52
|
+
const targetEntry = this.entries.get(target);
|
|
53
|
+
if (targetEntry === undefined) continue;
|
|
54
|
+
this.copyKey(entity, toArray(relation.foreignKey), targetEntry, changed);
|
|
55
|
+
}
|
|
56
|
+
for (const { parent, foreignKey } of this.parents.get(entry.entity) ?? []) {
|
|
57
|
+
if (parent.state === EntityState.Deleted) continue;
|
|
58
|
+
this.copyKey(entity, foreignKey, parent, changed);
|
|
59
|
+
}
|
|
60
|
+
return changed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private copyKey(entity: Values, foreignKey: readonly string[], target: TrackedEntry, changed: string[]): void {
|
|
64
|
+
const key = target.model.key;
|
|
65
|
+
if (key.length !== foreignKey.length) return;
|
|
66
|
+
const source = target.entity as Values;
|
|
67
|
+
key.forEach((property, index) => {
|
|
68
|
+
const name = foreignKey[index]!;
|
|
69
|
+
const value = source[property.propertyName];
|
|
70
|
+
if (entity[name] !== value) {
|
|
71
|
+
entity[name] = value;
|
|
72
|
+
changed.push(name);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toArray(value: string | readonly string[]): readonly string[] {
|
|
79
|
+
return typeof value === "string" ? [value] : value;
|
|
80
|
+
}
|
|
@@ -342,6 +342,11 @@ export class ChangeTracker {
|
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/** Non-barrel access to every tracked entry, for the save-time navigation fix-up. */
|
|
346
|
+
export function trackedEntriesOf(tracker: ChangeTracker): ReadonlyMap<object, TrackedEntry> {
|
|
347
|
+
return trackerEntries.get(tracker) ?? new Map();
|
|
348
|
+
}
|
|
349
|
+
|
|
345
350
|
/** Non-barrel helper used only by immediate DML admission. */
|
|
346
351
|
export function hasTrackedEntriesForModel(tracker: ChangeTracker, model: EntityModel): boolean {
|
|
347
352
|
return [...(trackerEntries.get(tracker)?.values() ?? [])].some((entry) => entry.model === model);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.98.
|
|
3
|
+
"version": "0.98.17",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|