bazis 0.98.27 → 0.98.28
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/di/ServiceProvider.ts +24 -1
- package/core/di/internal/resolveDependencyList.ts +19 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/orm/buildOrmProviders.ts +32 -7
- package/core/orm/ormModule.ts +3 -1
- package/core/orm/registerRepositories.ts +3 -1
- package/library/orm/DbContext.ts +1 -1
- package/library/orm/Metadata/ModelBuilder.ts +1 -0
- package/library/orm/Metadata/decorators.ts +22 -4
- package/library/orm/Metadata/types.ts +2 -0
- package/library/orm/Query/EntityQuery.ts +2 -1
- package/library/orm/Query/ImmediateMutations.ts +5 -4
- package/library/orm/Query/IncludeLoader.ts +3 -2
- package/library/orm/Query/SqlTranslator.ts +3 -1
- package/library/orm/Query/queryFilters.ts +16 -0
- package/library/orm/runtime.ts +2 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ import { ServiceRegistry } from "./internal/ServiceRegistry";
|
|
|
15
15
|
import { ResolutionTracker, type ResolutionActivation } from "./internal/ResolutionTracker";
|
|
16
16
|
import { ScopeLifecycle } from "./internal/ScopeLifecycle";
|
|
17
17
|
import { ServiceScope } from "./ServiceScope";
|
|
18
|
+
import { RESOLVE_DEPENDENCY_LIST } from "./internal/resolveDependencyList";
|
|
18
19
|
import { getConstructorDeps, getProviderDeps } from "./internal/providerDeps";
|
|
19
20
|
import type { BuildServiceProviderOptions, ServiceKey, ServiceResolver } from "./types";
|
|
20
21
|
import type {
|
|
@@ -550,7 +551,29 @@ export class ServiceProvider implements ServiceResolver {
|
|
|
550
551
|
return this.tryResolveInternal(token, key, scopeState, stack(), ownerLifetime);
|
|
551
552
|
},
|
|
552
553
|
has: (token: Token<unknown>, key?: ServiceKey): boolean => this.has(token, key),
|
|
553
|
-
|
|
554
|
+
[RESOLVE_DEPENDENCY_LIST]: (key: object, deps: ProviderDependencyList): unknown[] => {
|
|
555
|
+
assertOriginScopeLive();
|
|
556
|
+
return this.resolveListIn(key, deps, scopeState, stack(), ownerLifetime);
|
|
557
|
+
},
|
|
558
|
+
} as ServiceResolver;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** @internal See resolveDependencyList: the root resolver is the provider itself. */
|
|
562
|
+
[RESOLVE_DEPENDENCY_LIST](key: object, deps: ProviderDependencyList): unknown[] {
|
|
563
|
+
return this.resolveListIn(key, deps, this.lifetime.root, [], undefined);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// A dependency list resolved outside a registered provider (a DbContext's
|
|
567
|
+
// generated constructor dependencies). The synthetic provider is cached per
|
|
568
|
+
// key, so its plan is built once like any provider's.
|
|
569
|
+
private readonly listProviders = new WeakMap<object, Provider<unknown>>();
|
|
570
|
+
private resolveListIn(key: object, deps: ProviderDependencyList, scopeState: ResolutionScopeState, stack: ResolutionActivation[], ownerLifetime: ProviderLifetime | undefined): unknown[] {
|
|
571
|
+
let provider = this.listProviders.get(key);
|
|
572
|
+
if (provider === undefined || getProviderDeps(provider) !== deps) {
|
|
573
|
+
provider = { provide: key as Token<unknown>, useFactory: () => undefined, deps } as unknown as Provider<unknown>;
|
|
574
|
+
this.listProviders.set(key, provider);
|
|
575
|
+
}
|
|
576
|
+
return this.resolveDeps(this.getPlan(provider), scopeState, stack, ownerLifetime ?? "scoped");
|
|
554
577
|
}
|
|
555
578
|
|
|
556
579
|
private resolveDeps(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProviderDependencyList } from "../provider";
|
|
2
|
+
import type { ServiceResolver } from "../types";
|
|
3
|
+
|
|
4
|
+
/** @internal A resolver created by ServiceProvider resolves a dependency list in its own scope. */
|
|
5
|
+
export const RESOLVE_DEPENDENCY_LIST = Symbol("bazis.di.resolveDependencyList");
|
|
6
|
+
|
|
7
|
+
type ListResolver = ServiceResolver & { readonly [RESOLVE_DEPENDENCY_LIST]?: (key: object, deps: ProviderDependencyList) => unknown[] };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves `deps` (tokens, keyed, named, lazy or optional dependencies, as
|
|
11
|
+
* codegen records them) in the scope of `resolver`. `key` identifies the list
|
|
12
|
+
* so its resolution plan is built once. For framework factories that construct
|
|
13
|
+
* a class with generated dependencies, such as a DbContext.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveDependencyList(resolver: ServiceResolver, key: object, deps: ProviderDependencyList): unknown[] {
|
|
16
|
+
const resolve = (resolver as ListResolver)[RESOLVE_DEPENDENCY_LIST];
|
|
17
|
+
if (resolve === undefined) throw new TypeError("resolveDependencyList() needs a resolver created by the container");
|
|
18
|
+
return resolve(key, deps);
|
|
19
|
+
}
|
|
@@ -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.28" }, 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,6 +1,8 @@
|
|
|
1
|
-
import { DI, HOSTED_SERVICE, type InjectionToken, type ModuleOwnedContributionSnapshot, type ProviderDefinition } from "../di";
|
|
1
|
+
import { DI, HOSTED_SERVICE, type InjectionToken, type ModuleOwnedContributionSnapshot, type ProviderDefinition, type ProviderDependencyList, type ServiceResolver } from "../di";
|
|
2
|
+
import { getGeneratedClassDeps } from "../di/module/autoDeps";
|
|
3
|
+
import { resolveDependencyList } from "../di/internal/resolveDependencyList";
|
|
2
4
|
import { HEALTH_CHECK } from "../kernel";
|
|
3
|
-
import { DbContextOptions, type DatabaseProvider, type DbContext } from "../../library/orm";
|
|
5
|
+
import { DbContextOptions, OrmError, type DatabaseProvider, type DbContext } from "../../library/orm";
|
|
4
6
|
import { OrmLifecycle } from "./OrmLifecycle";
|
|
5
7
|
import { ownedStoreLifecycleFor, ownedStoreLifecycleHealthFor } from "./OrmOwnedStoreLifecycle";
|
|
6
8
|
import { effectiveOrmModuleConfig, ownedStoreRegistrations } from "./ownedStoreContributions";
|
|
@@ -9,7 +11,9 @@ import { DATABASE_PROVIDER } from "./DATABASE_PROVIDER";
|
|
|
9
11
|
import type { OrmModuleConfig } from "./ormModule";
|
|
10
12
|
|
|
11
13
|
type EntityClass = new () => object;
|
|
12
|
-
|
|
14
|
+
/** A DbContext subclass: `DbContextOptions` first, then its own constructor dependencies (since 0.98.28). */
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
type ContextClass<TContext extends DbContext> = new (options: DbContextOptions, ...dependencies: any[]) => TContext;
|
|
13
17
|
|
|
14
18
|
type OrmBuildConfig<TContext extends DbContext> = OrmModuleConfig<TContext> & {
|
|
15
19
|
readonly context: ContextClass<TContext>;
|
|
@@ -27,6 +31,27 @@ function createContextOptions<TContext extends DbContext>(
|
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
const contextDependencies = new WeakMap<object, ProviderDependencyList>();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a context with `options` and the rest of its constructor
|
|
38
|
+
* dependencies (generated by codegen) resolved in the request scope, so a
|
|
39
|
+
* context may read the current user or tenant: `constructor(options:
|
|
40
|
+
* DbContextOptions, private readonly user: CurrentUser)`.
|
|
41
|
+
*/
|
|
42
|
+
function constructContext<TContext extends DbContext>(context: ContextClass<TContext>, options: DbContextOptions, resolver: ServiceResolver): TContext {
|
|
43
|
+
const generated = getGeneratedClassDeps(context);
|
|
44
|
+
if (generated === undefined || generated.length === 0) return new context(options);
|
|
45
|
+
const first = generated[0];
|
|
46
|
+
if (first !== DbContextOptions && !(typeof first === "function" && first.name === "DbContextOptions")) {
|
|
47
|
+
throw new OrmError(`${context.name}: the first constructor parameter of a DbContext must be DbContextOptions; put its other dependencies after it.`);
|
|
48
|
+
}
|
|
49
|
+
if (generated.length === 1) return new context(options);
|
|
50
|
+
let rest = contextDependencies.get(context);
|
|
51
|
+
if (rest === undefined) { rest = generated.slice(1); contextDependencies.set(context, rest); }
|
|
52
|
+
return new context(options, ...resolveDependencyList(resolver, context, rest));
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
function createOrmLifecycle<TContext extends DbContext>(
|
|
31
56
|
config: OrmBuildConfig<TContext>,
|
|
32
57
|
options: DbContextOptions,
|
|
@@ -72,9 +97,9 @@ export function buildOrmModuleProviders<TContext extends DbContext>(
|
|
|
72
97
|
const token = providerOrToken;
|
|
73
98
|
const providers: ProviderDefinition[] = [
|
|
74
99
|
DI.scoped(
|
|
75
|
-
DI.
|
|
100
|
+
DI.factoryProviderWithResolver(config.context, [token, SERVICE_PROVIDER] as const, (resolver, provider: DatabaseProvider, serviceProvider) => {
|
|
76
101
|
const current = effectiveConfigFor(serviceProvider, registrationIdentity, config);
|
|
77
|
-
return
|
|
102
|
+
return constructContext(current.context, createContextOptions(current, provider), resolver);
|
|
78
103
|
}),
|
|
79
104
|
),
|
|
80
105
|
DI.singleton(
|
|
@@ -105,10 +130,10 @@ export function buildOrmModuleProviders<TContext extends DbContext>(
|
|
|
105
130
|
|
|
106
131
|
const provider = providerOrToken;
|
|
107
132
|
const providers: ProviderDefinition[] = [
|
|
108
|
-
DI.scoped(DI.
|
|
133
|
+
DI.scoped(DI.factoryProviderWithResolver(config.context, [SERVICE_PROVIDER], (resolver, serviceProvider) => {
|
|
109
134
|
const current = effectiveConfigFor(serviceProvider, registrationIdentity, config);
|
|
110
135
|
const context = current === config ? config.context : current.context;
|
|
111
|
-
return
|
|
136
|
+
return constructContext(context, current === config && optionsAtModuleCall !== undefined ? optionsAtModuleCall : createContextOptions(current, provider), resolver);
|
|
112
137
|
})),
|
|
113
138
|
// Standalone mode: the context owns its provider and closes it.
|
|
114
139
|
DI.singleton(DI.factoryProvider(HOSTED_SERVICE, [SERVICE_PROVIDER], (serviceProvider) => {
|
package/core/orm/ormModule.ts
CHANGED
|
@@ -20,7 +20,9 @@ import { OrmConnectionLifecycle } from "./OrmLifecycle";
|
|
|
20
20
|
import { attachOrmGraphContribution, attachOwnedStoreRegistration } from "./ownedStoreContributions";
|
|
21
21
|
|
|
22
22
|
type EntityClass = new () => object;
|
|
23
|
-
|
|
23
|
+
/** A DbContext subclass: `DbContextOptions` first, then its own constructor dependencies (since 0.98.28). */
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
type ContextClass<TContext extends DbContext> = new (options: DbContextOptions, ...dependencies: any[]) => TContext;
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* The single ORM registration shape. One function, three scenarios depending
|
|
@@ -3,7 +3,9 @@ import { Repository, type DbContext, type DbContextOptions } from "../../library
|
|
|
3
3
|
import { repositoryFor } from "./repository";
|
|
4
4
|
|
|
5
5
|
type EntityClass = new () => object;
|
|
6
|
-
|
|
6
|
+
/** A DbContext subclass: `DbContextOptions` first, then its own constructor dependencies (since 0.98.28). */
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
type ContextClass<TContext extends DbContext> = new (options: DbContextOptions, ...dependencies: any[]) => TContext;
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Registers a scoped `IRepository<T>` for each context entity and the
|
package/library/orm/DbContext.ts
CHANGED
|
@@ -36,7 +36,7 @@ export abstract class DbContext {
|
|
|
36
36
|
this.#options = options;
|
|
37
37
|
this.changeTracker = new ChangeTracker();
|
|
38
38
|
const provider = observedProvider(this, options.provider);
|
|
39
|
-
this.#runtime = { provider, models: options.model, tracker: this.changeTracker, runImmediateOperation: (operation) => monitorImmediateOperation(this, operation) };
|
|
39
|
+
this.#runtime = { context: this, provider, models: options.model, tracker: this.changeTracker, runImmediateOperation: (operation) => monitorImmediateOperation(this, operation) };
|
|
40
40
|
this.database = new DatabaseFacade(provider, options.model);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -143,6 +143,7 @@ export class ModelBuilder {
|
|
|
143
143
|
relations,
|
|
144
144
|
foreignKeys,
|
|
145
145
|
queryFilters: raw.queryFilters ?? [],
|
|
146
|
+
contextQueryFilters: raw.contextQueryFilters ?? [],
|
|
146
147
|
softDeleteProperty: raw.softDeleteProperty,
|
|
147
148
|
propertyByName: (name: string) => byName.get(name),
|
|
148
149
|
relationByName: (name: string) => relationByName.get(name),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ColumnOptionsType, RelationKind, PropertyConvention } from "./types";
|
|
2
2
|
import type { ValueConverter } from "./ValueConverter";
|
|
3
|
-
import { evaluatePredicate, type PredicateFn } from "../Query/conditions";
|
|
3
|
+
import { evaluatePredicate, type FieldSelector, type PredicateFn, type Predicate } from "../Query/conditions";
|
|
4
4
|
import type { Condition } from "../Query/conditions";
|
|
5
5
|
import { compileCheck, type CheckPredicate } from "../Schema/CheckExpression";
|
|
6
6
|
|
|
@@ -114,6 +114,8 @@ export interface RawEntity {
|
|
|
114
114
|
properties: Map<string, RawProperty>;
|
|
115
115
|
relations: RawRelation[];
|
|
116
116
|
queryFilters?: Condition[];
|
|
117
|
+
/** `@QueryFilter((entity, context) => …)`: evaluated per query with the context. */
|
|
118
|
+
contextQueryFilters?: ContextQueryFilter[];
|
|
117
119
|
/** Soft-delete property name (see `@SoftDelete` / `@Entity({ softDelete })`). */
|
|
118
120
|
softDeleteProperty?: string;
|
|
119
121
|
keyDeclaration?: { properties: readonly string[]; name?: string; anchor: string; composite: boolean };
|
|
@@ -135,7 +137,7 @@ function cloneRaw(source: RawEntity): RawEntity {
|
|
|
135
137
|
for (const [name, prop] of source.properties) {
|
|
136
138
|
properties.set(name, { ...prop, index: prop.index ? { ...prop.index } : undefined });
|
|
137
139
|
}
|
|
138
|
-
return { ...source, properties, relations: source.relations.map((relation) => ({ ...relation })), keyDeclaration: source.keyDeclaration && { ...source.keyDeclaration, properties: [...source.keyDeclaration.properties] }, indexes: source.indexes?.map((index) => ({ ...index, properties: [...index.properties] })), foreignKeys: source.foreignKeys?.map((foreignKey) => ({ ...foreignKey, properties: [...foreignKey.properties] })), checks: source.checks?.map((check) => ({ ...check })), queryFilters: source.queryFilters?.map(cloneCondition) };
|
|
140
|
+
return { ...source, properties, relations: source.relations.map((relation) => ({ ...relation })), keyDeclaration: source.keyDeclaration && { ...source.keyDeclaration, properties: [...source.keyDeclaration.properties] }, indexes: source.indexes?.map((index) => ({ ...index, properties: [...index.properties] })), foreignKeys: source.foreignKeys?.map((foreignKey) => ({ ...foreignKey, properties: [...foreignKey.properties] })), checks: source.checks?.map((check) => ({ ...check })), queryFilters: source.queryFilters?.map(cloneCondition), contextQueryFilters: source.contextQueryFilters && [...source.contextQueryFilters] };
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
function cloneCondition(condition: Condition): Condition {
|
|
@@ -364,14 +366,30 @@ export function HasConversion(converter: ValueConverter) {
|
|
|
364
366
|
};
|
|
365
367
|
}
|
|
366
368
|
|
|
369
|
+
/** A query filter that reads the `DbContext` of the query (a tenant, the current user). */
|
|
370
|
+
export type ContextQueryFilter = (entity: FieldSelector<never>, context: never) => Predicate;
|
|
371
|
+
|
|
367
372
|
/**
|
|
368
373
|
* Global query filter for the entity (multi-tenant, flags and so on).
|
|
369
374
|
* Applied automatically; disabled with `ignoreQueryFilters()`.
|
|
375
|
+
*
|
|
376
|
+
* With one parameter the filter is fixed when the class is declared:
|
|
377
|
+
* `@QueryFilter<Note>((n) => n.archived.eq(false))`. With a second parameter
|
|
378
|
+
* it receives the `DbContext` of each query and is evaluated then:
|
|
379
|
+
* `@QueryFilter<Note, NotesDb>((n, db) => n.tenantId.eq(db.tenantId))`. If it
|
|
380
|
+
* throws, the query fails instead of running unfiltered.
|
|
370
381
|
*/
|
|
371
|
-
|
|
382
|
+
type QueryFilterDecorator = (value: abstract new (...args: never[]) => unknown, context: ClassDecoratorContext) => void;
|
|
383
|
+
export function QueryFilter<T extends object>(predicate: PredicateFn<T>): QueryFilterDecorator;
|
|
384
|
+
export function QueryFilter<T extends object, C extends object>(predicate: (entity: FieldSelector<T>, context: C) => Predicate): QueryFilterDecorator;
|
|
385
|
+
export function QueryFilter<T extends object>(predicate: PredicateFn<T> | ((entity: FieldSelector<T>, context: never) => Predicate)): QueryFilterDecorator {
|
|
372
386
|
return (_value: abstract new (...args: never[]) => unknown, context: ClassDecoratorContext): void => {
|
|
373
387
|
const raw = ownRaw(context.metadata);
|
|
374
|
-
|
|
388
|
+
if (predicate.length >= 2) {
|
|
389
|
+
raw.contextQueryFilters = [...(raw.contextQueryFilters ?? []), predicate as unknown as ContextQueryFilter];
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const node = evaluatePredicate(predicate as PredicateFn<T>, "@QueryFilter").node;
|
|
375
393
|
raw.queryFilters = [...(raw.queryFilters ?? []), node];
|
|
376
394
|
};
|
|
377
395
|
}
|
|
@@ -131,6 +131,8 @@ export interface EntityModel {
|
|
|
131
131
|
readonly foreignKeys: readonly ForeignKeyModel[];
|
|
132
132
|
/** Global query filters (`@QueryFilter`), applied to every SELECT. */
|
|
133
133
|
readonly queryFilters: readonly Condition[];
|
|
134
|
+
/** Query filters that read the query's `DbContext`; evaluated per query (see `effectiveQueryFilters`). */
|
|
135
|
+
readonly contextQueryFilters?: readonly import("./decorators").ContextQueryFilter[];
|
|
134
136
|
/** Soft-delete property name (NULL = not deleted). `@SoftDelete` / `@Entity({ softDelete })`. */
|
|
135
137
|
readonly softDeleteProperty?: string;
|
|
136
138
|
/** Property by name. */
|
|
@@ -6,6 +6,7 @@ import { IncludeLoader } from "./IncludeLoader";
|
|
|
6
6
|
import { materialize, materializeProjection } from "./materialize";
|
|
7
7
|
import { EMPTY_PLAN, withCondition, withOrder, type QueryPlan } from "./QueryPlan";
|
|
8
8
|
import { SqlTranslator } from "./SqlTranslator";
|
|
9
|
+
import { effectiveQueryFilters } from "./queryFilters";
|
|
9
10
|
import { executeImmediateDelete, executeImmediateUpdate, type OrmMutationResultV1, type OrmUpdateValuesV1 } from "./ImmediateMutations";
|
|
10
11
|
|
|
11
12
|
export interface ForUpdateOptionsV1 { readonly skipLocked?: boolean; }
|
|
@@ -144,7 +145,7 @@ export class EntityQuery<T extends object, TResult = T> {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
protected translator(): SqlTranslator {
|
|
147
|
-
return new SqlTranslator(this.model, this.runtime.provider.dialect);
|
|
148
|
+
return new SqlTranslator(this.model, this.runtime.provider.dialect, this.plan.ignoreQueryFilters ? [] : effectiveQueryFilters(this.model, this.runtime.context));
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
async toList(): Promise<TResult[]> {
|
|
@@ -8,6 +8,7 @@ import type { SqlParam } from "../Providers/types";
|
|
|
8
8
|
import type { QueryPlan } from "./QueryPlan";
|
|
9
9
|
import { Operand, type Condition, type FieldSelector } from "./conditions";
|
|
10
10
|
import { SqlTranslator } from "./SqlTranslator";
|
|
11
|
+
import { effectiveQueryFilters } from "./queryFilters";
|
|
11
12
|
|
|
12
13
|
export interface OrmMutationResultV1 { readonly affectedRows: number; }
|
|
13
14
|
export interface OrmInsertIfAbsentResultV1 { readonly inserted: boolean; }
|
|
@@ -17,7 +18,7 @@ export type OrmUniqueKeySelectorV1<T extends object> = (entity: FieldSelector<T>
|
|
|
17
18
|
export function executeImmediateUpdate<T extends object>(model: EntityModel, runtime: DbContextRuntime, plan: QueryPlan, values: OrmUpdateValuesV1<T>): Promise<OrmMutationResultV1> {
|
|
18
19
|
return runtime.runImmediateOperation(async () => {
|
|
19
20
|
guard(model, runtime); const admittedPlan = admitPlan(plan, "executeUpdate");
|
|
20
|
-
const conditions = effectiveConditions(model, admittedPlan); const assignments = encodeAssignments(runtime, admitUpdateValues(model, values));
|
|
21
|
+
const conditions = effectiveConditions(model, admittedPlan, runtime); const assignments = encodeAssignments(runtime, admitUpdateValues(model, values));
|
|
21
22
|
const compiled = new SqlTranslator(model, runtime.provider.dialect).immediateUpdate(assignments, conditions, (property, value) => encode(runtime, property, snapshot(value), true));
|
|
22
23
|
Object.freeze(compiled.params);
|
|
23
24
|
guard(model, runtime);
|
|
@@ -28,7 +29,7 @@ export function executeImmediateUpdate<T extends object>(model: EntityModel, run
|
|
|
28
29
|
|
|
29
30
|
export function executeImmediateDelete(model: EntityModel, runtime: DbContextRuntime, plan: QueryPlan): Promise<OrmMutationResultV1> {
|
|
30
31
|
return runtime.runImmediateOperation(async () => {
|
|
31
|
-
guard(model, runtime); const admittedPlan = admitPlan(plan, "executeDelete"); const conditions = effectiveConditions(model, admittedPlan);
|
|
32
|
+
guard(model, runtime); const admittedPlan = admitPlan(plan, "executeDelete"); const conditions = effectiveConditions(model, admittedPlan, runtime);
|
|
32
33
|
const compiled = new SqlTranslator(model, runtime.provider.dialect).immediateDelete(conditions, (property, value) => encode(runtime, property, snapshot(value), true));
|
|
33
34
|
Object.freeze(compiled.params);
|
|
34
35
|
guard(model, runtime);
|
|
@@ -177,8 +178,8 @@ function conflictTarget<T extends object>(model: EntityModel, options: { readonl
|
|
|
177
178
|
return Object.freeze(columns as string[]);
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
function effectiveConditions(model: EntityModel, plan: AdmittedImmediatePlan): readonly Condition[] {
|
|
181
|
-
const source: Condition[] = plan.ignoreQueryFilters ? [] : readConditionList(model.
|
|
181
|
+
function effectiveConditions(model: EntityModel, plan: AdmittedImmediatePlan, runtime: DbContextRuntime): readonly Condition[] {
|
|
182
|
+
const source: Condition[] = plan.ignoreQueryFilters ? [] : readConditionList([...effectiveQueryFilters(model, runtime.context)]);
|
|
182
183
|
if (!plan.ignoreQueryFilters && model.softDeleteProperty) source.push({ kind: "null", property: model.softDeleteProperty, negated: false });
|
|
183
184
|
source.push(...plan.explicitConditions); const active = new Set<object>(); const memo = new WeakMap<object, Condition>();
|
|
184
185
|
return Object.freeze(source.map((condition) => cloneCondition(model, condition, active, memo)));
|
|
@@ -6,6 +6,7 @@ import type { DbContextRuntime } from "../runtime";
|
|
|
6
6
|
import { materialize } from "./materialize";
|
|
7
7
|
import { EMPTY_PLAN } from "./QueryPlan";
|
|
8
8
|
import { SqlTranslator } from "./SqlTranslator";
|
|
9
|
+
import { effectiveQueryFilters } from "./queryFilters";
|
|
9
10
|
|
|
10
11
|
interface Level {
|
|
11
12
|
readonly model: EntityModel;
|
|
@@ -156,7 +157,7 @@ export class IncludeLoader {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
private async queryIn(model: EntityModel, property: string, values: readonly unknown[], noTracking: boolean): Promise<object[]> {
|
|
159
|
-
const translator = new SqlTranslator(model, this.runtime.provider.dialect);
|
|
160
|
+
const translator = new SqlTranslator(model, this.runtime.provider.dialect, effectiveQueryFilters(model, this.runtime.context));
|
|
160
161
|
const result: object[] = [];
|
|
161
162
|
const chunkSize = maxParametersPerInList(this.runtime.provider);
|
|
162
163
|
for (let start = 0; start < values.length; start += chunkSize) {
|
|
@@ -180,7 +181,7 @@ export class IncludeLoader {
|
|
|
180
181
|
|
|
181
182
|
private async queryTuples(model: EntityModel, properties: readonly string[], values: readonly (readonly unknown[])[], noTracking: boolean): Promise<object[]> {
|
|
182
183
|
const result: object[] = []; const perTuple = properties.length; const chunkSize = Math.max(1, Math.floor(maxParametersPerInList(this.runtime.provider) / perTuple));
|
|
183
|
-
for (let start = 0; start < values.length; start += chunkSize) { const translator = new SqlTranslator(model, this.runtime.provider.dialect); const { sql, params } = translator.selectAll({ ...EMPTY_PLAN, conditions: [{ kind: "tuples", properties, values: values.slice(start, start + chunkSize) }], noTracking }); for (let entity of (await this.runtime.provider.query(sql, params)).map((row) => materialize<object>(model, row, this.runtime.provider.dialect))) { if (!noTracking) entity = this.runtime.tracker.trackLoaded(entity, model); result.push(entity); } }
|
|
184
|
+
for (let start = 0; start < values.length; start += chunkSize) { const translator = new SqlTranslator(model, this.runtime.provider.dialect, effectiveQueryFilters(model, this.runtime.context)); const { sql, params } = translator.selectAll({ ...EMPTY_PLAN, conditions: [{ kind: "tuples", properties, values: values.slice(start, start + chunkSize) }], noTracking }); for (let entity of (await this.runtime.provider.query(sql, params)).map((row) => materialize<object>(model, row, this.runtime.provider.dialect))) { if (!noTracking) entity = this.runtime.tracker.trackLoaded(entity, model); result.push(entity); } }
|
|
184
185
|
return result;
|
|
185
186
|
}
|
|
186
187
|
private tuple(model: EntityModel, values: readonly unknown[]): string | undefined {
|
|
@@ -35,9 +35,11 @@ export function quotedColumnList(model: EntityModel, dialect: SqlDialect): strin
|
|
|
35
35
|
* parameter array (no concatenation of user data).
|
|
36
36
|
*/
|
|
37
37
|
export class SqlTranslator {
|
|
38
|
+
/** `filters`: the query filters to apply (see `effectiveQueryFilters`); the model's fixed ones by default. */
|
|
38
39
|
constructor(
|
|
39
40
|
private readonly model: EntityModel,
|
|
40
41
|
private readonly dialect: SqlDialect,
|
|
42
|
+
private readonly filters: readonly Condition[] = model.queryFilters,
|
|
41
43
|
) {}
|
|
42
44
|
|
|
43
45
|
/** SELECT of all columns (or a projection) with WHERE/ORDER/LIMIT/OFFSET. */
|
|
@@ -98,7 +100,7 @@ export class SqlTranslator {
|
|
|
98
100
|
private whereClause(plan: QueryPlan, params: SqlParam[], encode: ConditionValueEncoder = (property, value) => encodeProperty(property, value, this.dialect)): string {
|
|
99
101
|
const parts: string[] = [];
|
|
100
102
|
if (!plan.ignoreQueryFilters) {
|
|
101
|
-
for (const filter of this.
|
|
103
|
+
for (const filter of this.filters) {
|
|
102
104
|
parts.push(this.condition(filter, params, encode));
|
|
103
105
|
}
|
|
104
106
|
if (this.model.softDeleteProperty) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityModel } from "../Metadata/types";
|
|
2
|
+
import { evaluatePredicate, type Condition, type FieldSelector } from "./conditions";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The query filters of `model` for a query of `context`: the fixed ones plus
|
|
6
|
+
* the context filters evaluated now. A context filter that throws fails the
|
|
7
|
+
* query; it never runs without the filter.
|
|
8
|
+
*/
|
|
9
|
+
export function effectiveQueryFilters(model: EntityModel, context: object | undefined): readonly Condition[] {
|
|
10
|
+
const dynamic = model.contextQueryFilters ?? [];
|
|
11
|
+
if (dynamic.length === 0) return model.queryFilters;
|
|
12
|
+
return [
|
|
13
|
+
...model.queryFilters,
|
|
14
|
+
...dynamic.map((filter) => evaluatePredicate((entity) => filter(entity as FieldSelector<never>, context as never), "@QueryFilter").node),
|
|
15
|
+
];
|
|
16
|
+
}
|
package/library/orm/runtime.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { ChangeTracker } from "./Tracking/ChangeTracker";
|
|
|
7
7
|
* module to break import cycles between the context, the set and the tracker.
|
|
8
8
|
*/
|
|
9
9
|
export interface DbContextRuntime {
|
|
10
|
+
/** The DbContext that owns this runtime; context query filters read it. */
|
|
11
|
+
readonly context?: object;
|
|
10
12
|
readonly provider: DatabaseProvider;
|
|
11
13
|
readonly models: OrmModel;
|
|
12
14
|
readonly tracker: ChangeTracker;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.98.
|
|
3
|
+
"version": "0.98.28",
|
|
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",
|