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.
@@ -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.27" }, capabilities: { experimentalApi: true },
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
- type ContextClass<TContext extends DbContext> = new (options: DbContextOptions) => TContext;
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.factoryProvider(config.context, [token, SERVICE_PROVIDER] as const, (provider: DatabaseProvider, serviceProvider) => {
100
+ DI.factoryProviderWithResolver(config.context, [token, SERVICE_PROVIDER] as const, (resolver, provider: DatabaseProvider, serviceProvider) => {
76
101
  const current = effectiveConfigFor(serviceProvider, registrationIdentity, config);
77
- return new current.context(createContextOptions(current, provider));
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.factoryProvider(config.context, [SERVICE_PROVIDER], (serviceProvider) => {
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 new context(current === config && optionsAtModuleCall !== undefined ? optionsAtModuleCall : createContextOptions(current, provider));
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) => {
@@ -20,7 +20,9 @@ import { OrmConnectionLifecycle } from "./OrmLifecycle";
20
20
  import { attachOrmGraphContribution, attachOwnedStoreRegistration } from "./ownedStoreContributions";
21
21
 
22
22
  type EntityClass = new () => object;
23
- type ContextClass<TContext extends DbContext> = new (options: DbContextOptions) => TContext;
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
- type ContextClass<TContext extends DbContext> = new (options: DbContextOptions) => TContext;
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
@@ -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
- export function QueryFilter<T extends object>(predicate: PredicateFn<T>) {
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
- const node = evaluatePredicate(predicate, "@QueryFilter").node;
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.queryFilters);
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.model.queryFilters) {
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
+ }
@@ -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.27",
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",