@getstrata/core 0.5.100 → 0.5.101

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 0.5.101
4
+
5
+ - Tenant middleware falls back with explicit branches when a member, admin, or guest tenant lookup misses, so the request still scopes to the default tenant.
6
+
3
7
  ## 0.5.100
4
8
 
5
9
  HiroApp dogfood of 0.5.99: eager `with()` / nested `load("a.b")` missed related rows when a Postgres int4 PK arrived as a JS `number` and an int8 FK as a `bigint`. Map matching used `===`.
@@ -1,5 +1,5 @@
1
1
  import { type CursorPaginatedResult, type PaginatedResult } from "../pagination/index.ts";
2
- import { type BelongsToManyRelation, type BelongsToRelation, type HasManyRelation, type MorphManyRelation, type MorphOneRelation, type MorphToRelation } from "./relationships.ts";
2
+ import { type BelongsToManyRelation, type BelongsToRelation, type HasManyRelation, type HasManyThroughRelation, type MorphManyRelation, type MorphOneRelation, type MorphToRelation } from "./relationships.ts";
3
3
  import { RepositoryQuery } from "./repositoryQuery.ts";
4
4
  import type { TableDefinition } from "./table.ts";
5
5
  import type { MutationValues, QueryOptions, QueryWhere, UpdateValues } from "./types.ts";
@@ -59,6 +59,10 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
59
59
  }>>;
60
60
  findByHasManyRelation<TParent extends object, LocalKey extends keyof TParent & string, ForeignKey extends keyof TEntity & string>(relation: HasManyRelation<TParent, TEntity, LocalKey, ForeignKey>, parentId: TParent[LocalKey], options?: Omit<QueryOptions<TEntity>, "where">): Promise<TEntity[]>;
61
61
  loadHasManyForParents<TParent extends object, LocalKey extends keyof TParent & string, ForeignKey extends keyof TEntity & string>(parents: readonly TParent[], relation: HasManyRelation<TParent, TEntity, LocalKey, ForeignKey>, options?: Omit<QueryOptions<TEntity>, "where">): Promise<Map<TParent[LocalKey], TEntity[]>>;
62
+ findHasManyThrough<TParent extends object, LocalKey extends keyof TParent & string, FirstKey extends string, SecondLocalKey extends string, SecondKey extends keyof TEntity & string>(parentId: unknown, relation: HasManyThroughRelation<TParent, TEntity, LocalKey, FirstKey, SecondLocalKey, SecondKey>, options?: QueryOptions<TEntity>): Promise<TEntity[]>;
63
+ loadHasManyThroughForParents<TParent extends object, LocalKey extends keyof TParent & string, FirstKey extends string, SecondLocalKey extends string, SecondKey extends keyof TEntity & string>(parents: readonly TParent[], relation: HasManyThroughRelation<TParent, TEntity, LocalKey, FirstKey, SecondLocalKey, SecondKey>, options?: QueryOptions<TEntity>): Promise<Map<TParent[LocalKey], TEntity[]>>;
64
+ private throughSoftDeleteClause;
65
+ private buildThroughWhere;
62
66
  loadBelongsToForParents<TChild extends object, TParent extends object, ForeignKey extends keyof TChild & string, OwnerKey extends keyof TParent & string>(children: readonly TChild[], relation: BelongsToRelation<TChild, TParent, ForeignKey, OwnerKey>, parentRepository: BaseRepository<TParent, OwnerKey>, options?: Omit<QueryOptions<TParent>, "where">): Promise<Map<TChild[ForeignKey], TParent>>;
63
67
  loadMorphManyForParents<TParent extends object, LocalKey extends keyof TParent & string, MorphTypeKey extends keyof TEntity & string, MorphIdKey extends keyof TEntity & string>(parents: readonly TParent[], relation: MorphManyRelation<TParent, TEntity, LocalKey, MorphTypeKey, MorphIdKey>, options?: Omit<QueryOptions<TEntity>, "where">): Promise<Map<TParent[LocalKey], TEntity[]>>;
64
68
  loadMorphOneForParents<TParent extends object, LocalKey extends keyof TParent & string, MorphTypeKey extends keyof TEntity & string, MorphIdKey extends keyof TEntity & string>(parents: readonly TParent[], relation: MorphOneRelation<TParent, TEntity, LocalKey, MorphTypeKey, MorphIdKey>, options?: Omit<QueryOptions<TEntity>, "where">): Promise<Map<TParent[LocalKey], TEntity | undefined>>;
@@ -5,10 +5,10 @@ export { mapDatabaseError, withDatabaseErrorHandling } from "./errors.ts";
5
5
  export { Factory } from "./factory.ts";
6
6
  export { foreignKeyFromTable, pivotTableName, singularize } from "./inflection.ts";
7
7
  export type { CastType, GlobalScopeFn, ModelConstructor } from "./model.ts";
8
- export { applyCasts, BelongsToManyRelationQuery, BelongsToRelationQuery, dehydrateValue, filterMassAssignable, HasManyRelationQuery, HasOneRelationQuery, hydrateValue, Model, ModelQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, registerModelClass, registerModelRepository, } from "./model.ts";
8
+ export { applyCasts, BelongsToManyRelationQuery, BelongsToRelationQuery, dehydrateValue, filterMassAssignable, HasManyRelationQuery, HasManyThroughRelationQuery, HasOneRelationQuery, hydrateValue, Model, ModelQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, registerModelClass, registerModelRepository, } from "./model.ts";
9
9
  export { buildAdvancedWhereClause, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, } from "./query.ts";
10
- export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation, } from "./relationships.ts";
11
- export { belongsTo, belongsToMany, hasMany, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasOneRelation, indexMorphManyRelation, indexMorphOneRelation, indexMorphToRelation, morphMany, morphOne, morphTo, } from "./relationships.ts";
10
+ export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation, } from "./relationships.ts";
11
+ export { belongsTo, belongsToMany, hasMany, hasManyThrough, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasManyThroughRelation, indexHasOneRelation, indexMorphManyRelation, indexMorphOneRelation, indexMorphToRelation, morphMany, morphOne, morphTo, } from "./relationships.ts";
12
12
  export { RepositoryQuery } from "./repositoryQuery.ts";
13
13
  export type { BlueprintAction, BlueprintCallback, ColumnKind, DatabaseDriver, ForeignKeyOptions, Grammar, IndexDefinition, IndexKind, ResolveDatabaseDriverOptions, SchemaBuilder, } from "./schema/index.ts";
14
14
  export { Blueprint, ColumnDefinition, compileBlueprint, createSchemaBuilder, ForeignIdColumnDefinition, grammarForDriver, inferReferencedTable, MySqlGrammar, PostgresGrammar, resolveDatabaseDriver, Schema, SqliteGrammar, UnsupportedSchemaFeatureError, } from "./schema/index.ts";
@@ -1,6 +1,6 @@
1
1
  import type BaseRepository from "./baseRepository.ts";
2
2
  import type { AnyRelationQuery, RelatedModelClass } from "./relationQuery.ts";
3
- import { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuery, HasOneRelationQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery } from "./relationQuery.ts";
3
+ import { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuery, HasManyThroughRelationQuery, HasOneRelationQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery } from "./relationQuery.ts";
4
4
  import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation } from "./relationships.ts";
5
5
  import type { RepositoryQuery } from "./repositoryQuery.ts";
6
6
  import type { QueryOptions, QueryWhere } from "./types.ts";
@@ -56,6 +56,9 @@ declare class ModelQuery {
56
56
  withMorphMany(...args: Parameters<RepositoryQuery<Record<string, unknown>, "id">["withMorphMany"]>): this;
57
57
  withMorphOne(...args: Parameters<RepositoryQuery<Record<string, unknown>, "id">["withMorphOne"]>): this;
58
58
  withMorphTo(...args: Parameters<RepositoryQuery<Record<string, unknown>, "id">["withMorphTo"]>): this;
59
+ withHasManyThrough(...args: Parameters<RepositoryQuery<Record<string, unknown>, "id">["withHasManyThrough"]>): this;
60
+ withTrashed(): this;
61
+ onlyTrashed(): this;
59
62
  get(): Promise<Array<Model<Record<string, unknown>, "id">>>;
60
63
  first(): Promise<Model<Record<string, unknown>, "id"> | null>;
61
64
  find(id: unknown): Promise<Model<Record<string, unknown>, "id"> | null>;
@@ -103,6 +106,21 @@ declare class Model<TEntity extends object, PrimaryKey extends keyof TEntity & s
103
106
  static newFromRecord(this: object, record: object, exists?: boolean): Model<Record<string, unknown>, "id">;
104
107
  static create(this: object, attributes: Record<string, unknown>, forced?: Record<string, unknown>): Promise<Model<Record<string, unknown>, "id">>;
105
108
  static with(this: object, ...relations: string[]): ModelQuery;
109
+ static withTrashed(this: object): ModelQuery;
110
+ static onlyTrashed(this: object): ModelQuery;
111
+ static chunk(this: object, count: number, callback: (models: Array<Model<Record<string, unknown>, "id">>) => Promise<boolean | void>): Promise<void>;
112
+ static cursorPaginate(this: object, options: {
113
+ perPage: number;
114
+ cursor?: unknown;
115
+ }): Promise<{
116
+ data: Array<Model<Record<string, unknown>, "id">>;
117
+ meta: {
118
+ per_page: number;
119
+ next_cursor: unknown;
120
+ prev_cursor: unknown;
121
+ has_more: boolean;
122
+ };
123
+ }>;
106
124
  static whereHas(this: object, name: string, constrain?: (query: AnyRelationQuery) => void): ModelQuery;
107
125
  static has(this: object, name: string): ModelQuery;
108
126
  static doesntHave(this: object, name: string): ModelQuery;
@@ -127,6 +145,7 @@ declare class Model<TEntity extends object, PrimaryKey extends keyof TEntity & s
127
145
  hasMany<TRelated extends object, RelatedKey extends keyof TRelated & string>(related: RelatedRef<TRelated, RelatedKey>, foreignKey?: keyof TRelated & string, localKey?: PrimaryKey): HasManyRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
128
146
  hasOne<TRelated extends object, RelatedKey extends keyof TRelated & string>(related: RelatedRef<TRelated, RelatedKey>, foreignKey?: keyof TRelated & string, localKey?: PrimaryKey): HasOneRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
129
147
  belongsTo<TRelated extends object, RelatedKey extends keyof TRelated & string>(related: RelatedRef<TRelated, RelatedKey>, foreignKey?: keyof TEntity & string, ownerKey?: RelatedKey): BelongsToRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
148
+ hasManyThrough<TRelated extends object, TThrough extends object, RelatedKey extends keyof TRelated & string, ThroughKey extends keyof TThrough & string>(related: RelatedRef<TRelated, RelatedKey>, through: RelatedRef<TThrough, ThroughKey>, firstKey?: string, secondKey?: keyof TRelated & string, localKey?: PrimaryKey, secondLocalKey?: ThroughKey): HasManyThroughRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
130
149
  belongsToMany<TRelated extends object, RelatedKey extends keyof TRelated & string, Pivot extends object = Record<string, unknown>>(related: RelatedRef<TRelated, RelatedKey>, pivotTable?: string, foreignPivotKey?: keyof Pivot & string, relatedPivotKey?: keyof Pivot & string): BelongsToManyRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey, Pivot>;
131
150
  morphMany<TRelated extends object, RelatedKey extends keyof TRelated & string>(related: RelatedRef<TRelated, RelatedKey>, morphName: string, typeKey?: keyof TRelated & string, idKey?: keyof TRelated & string, morphType?: string): MorphManyRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
132
151
  morphOne<TRelated extends object, RelatedKey extends keyof TRelated & string>(related: RelatedRef<TRelated, RelatedKey>, morphName: string, typeKey?: keyof TRelated & string, idKey?: keyof TRelated & string, morphType?: string): MorphOneRelationQuery<TEntity, PrimaryKey, TRelated, RelatedKey>;
@@ -138,6 +157,6 @@ declare class Model<TEntity extends object, PrimaryKey extends keyof TEntity & s
138
157
  }
139
158
  /** Binds a Model class to its table/connection and names it for `hasMany("User")`. */
140
159
  declare function registerModelRepository<TModelClass>(model: TModelClass, repository: object): TModelClass;
141
- export { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuery, HasOneRelationQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, } from "./relationQuery.ts";
160
+ export { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuery, HasManyThroughRelationQuery, HasOneRelationQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, } from "./relationQuery.ts";
142
161
  export type { CastType, GlobalScopeFn, ModelClassType, ModelConstructor };
143
162
  export { applyCasts, dehydrateValue, filterMassAssignable, hydrateValue, Model, ModelQuery, registerModelClass, registerModelRepository, };
@@ -1,5 +1,5 @@
1
1
  import type BaseRepository from "./baseRepository.ts";
2
- import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation } from "./relationships.ts";
2
+ import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation } from "./relationships.ts";
3
3
  import type { RepositoryQuery } from "./repositoryQuery.ts";
4
4
  import type { QueryOptions, QueryWhere } from "./types.ts";
5
5
  type RelatedRecord = {
@@ -17,7 +17,7 @@ interface RelationHost<TEntity extends object, PrimaryKey extends keyof TEntity
17
17
  get<K extends keyof TEntity>(key: K): TEntity[K];
18
18
  getRepository(): BaseRepository<TEntity, PrimaryKey>;
19
19
  }
20
- type RelationKind = "hasMany" | "hasOne" | "belongsTo" | "belongsToMany" | "morphMany" | "morphOne" | "morphTo";
20
+ type RelationKind = "hasMany" | "hasOne" | "belongsTo" | "belongsToMany" | "hasManyThrough" | "morphMany" | "morphOne" | "morphTo";
21
21
  type ExistsClause = {
22
22
  sql: string;
23
23
  params: unknown[];
@@ -159,6 +159,25 @@ declare class MorphToRelationQuery<TChild extends object, ChildKey extends keyof
159
159
  get(): Promise<RelatedRecord | null>;
160
160
  then(onfulfilled?: ((value: RelatedRecord | null) => unknown) | null, onrejected?: ((reason: unknown) => unknown) | null): Promise<unknown>;
161
161
  }
162
+ declare class HasManyThroughRelationQuery<TParent extends object, ParentKey extends keyof TParent & string, TFar extends object, FarKey extends keyof TFar & string> {
163
+ private readonly parent;
164
+ private readonly related;
165
+ readonly relation: HasManyThroughRelation<TParent, TFar, ParentKey, string, string, keyof TFar & string>;
166
+ readonly kind: RelationKind;
167
+ private extraWhere;
168
+ private extraOptions;
169
+ constructor(parent: RelationHost<TParent, ParentKey>, related: RelatedModelClass<TFar, FarKey>, relation: HasManyThroughRelation<TParent, TFar, ParentKey, string, string, keyof TFar & string>);
170
+ where(where: QueryWhere<TFar>): this;
171
+ orderBy(orderBy: QueryOptions<TFar>["orderBy"]): this;
172
+ limit(limit: number): this;
173
+ applyEagerLoad(query: RepositoryQuery<TParent, ParentKey>, alias: string): void;
174
+ hydrateEager(row: Record<string, unknown>, alias: string): unknown;
175
+ toExistsClause(parentTable: string): ExistsClause;
176
+ get(): Promise<RelatedRecord[]>;
177
+ first(): Promise<RelatedRecord | null>;
178
+ count(): Promise<number>;
179
+ then(onfulfilled?: ((value: RelatedRecord[]) => unknown) | null, onrejected?: ((reason: unknown) => unknown) | null): Promise<unknown>;
180
+ }
162
181
  type AnyRelationQuery = {
163
182
  kind: RelationKind;
164
183
  applyEagerLoad(query: unknown, alias: string): void;
@@ -169,4 +188,4 @@ type AnyRelationQuery = {
169
188
  then?: (onfulfilled?: ((value: unknown) => unknown) | null, onrejected?: ((reason: unknown) => unknown) | null) => Promise<unknown>;
170
189
  };
171
190
  export type { AnyRelationQuery, RelatedModelClass, RelatedRecord, RelationHost };
172
- export { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuery, HasOneRelationQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, };
191
+ export { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuery, HasManyThroughRelationQuery, HasOneRelationQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, };
@@ -25,6 +25,16 @@ interface BelongsToManyRelation<TParent, TRelated, Pivot extends object, ParentK
25
25
  foreignPivotKey: ForeignPivotKey;
26
26
  relatedPivotKey: RelatedPivotKey;
27
27
  }
28
+ interface HasManyThroughRelation<TParent, TFar, LocalKey extends keyof TParent & string = keyof TParent & string, FirstKey extends string = string, SecondLocalKey extends string = string, SecondKey extends keyof TFar & string = keyof TFar & string> {
29
+ type: "hasManyThrough";
30
+ name: string;
31
+ throughTable: string;
32
+ localKey: LocalKey;
33
+ firstKey: FirstKey;
34
+ secondLocalKey: SecondLocalKey;
35
+ secondKey: SecondKey;
36
+ throughParentKey?: string;
37
+ }
28
38
  declare function hasMany<TParent, TChild, LocalKey extends keyof TParent & string = keyof TParent & string, ForeignKey extends keyof TChild & string = keyof TChild & string>(definition: {
29
39
  name: string;
30
40
  localKey: LocalKey;
@@ -40,6 +50,15 @@ declare function belongsTo<TChild, TParent, ForeignKey extends keyof TChild & st
40
50
  foreignKey: ForeignKey;
41
51
  ownerKey: OwnerKey;
42
52
  }): BelongsToRelation<TChild, TParent, ForeignKey, OwnerKey>;
53
+ declare function hasManyThrough<TParent, TFar, LocalKey extends keyof TParent & string = keyof TParent & string, FirstKey extends string = string, SecondLocalKey extends string = string, SecondKey extends keyof TFar & string = keyof TFar & string>(definition: {
54
+ name: string;
55
+ throughTable: string;
56
+ localKey: LocalKey;
57
+ firstKey: FirstKey;
58
+ secondLocalKey: SecondLocalKey;
59
+ secondKey: SecondKey;
60
+ throughParentKey?: string;
61
+ }): HasManyThroughRelation<TParent, TFar, LocalKey, FirstKey, SecondLocalKey, SecondKey>;
43
62
  declare function belongsToMany<TParent, TRelated, Pivot extends object, ParentKey extends keyof TParent & string = keyof TParent & string, RelatedKey extends keyof TRelated & string = keyof TRelated & string, ForeignPivotKey extends keyof Pivot & string = keyof Pivot & string, RelatedPivotKey extends keyof Pivot & string = keyof Pivot & string>(definition: {
44
63
  name: string;
45
64
  pivotTable: string;
@@ -97,6 +116,7 @@ declare function morphTo<TChild, MorphTypeKey extends keyof TChild & string = ke
97
116
  }): MorphToRelation<TChild, MorphTypeKey, MorphIdKey>;
98
117
  declare function indexMorphManyRelation<TParent, TChild, LocalKey extends keyof TParent & string, MorphTypeKey extends keyof TChild & string, MorphIdKey extends keyof TChild & string>(parents: readonly TParent[], children: readonly TChild[], relation: MorphManyRelation<TParent, TChild, LocalKey, MorphTypeKey, MorphIdKey>): Map<TParent[LocalKey], TChild[]>;
99
118
  declare function indexMorphOneRelation<TParent, TChild, LocalKey extends keyof TParent & string, MorphTypeKey extends keyof TChild & string, MorphIdKey extends keyof TChild & string>(parents: readonly TParent[], children: readonly TChild[], relation: MorphOneRelation<TParent, TChild, LocalKey, MorphTypeKey, MorphIdKey>): Map<TParent[LocalKey], TChild | undefined>;
119
+ declare function indexHasManyThroughRelation<TParent, TFar, LocalKey extends keyof TParent & string, FirstKey extends string, SecondLocalKey extends string, SecondKey extends keyof TFar & string>(parents: readonly TParent[], children: readonly (TFar & Record<string, unknown>)[], relation: HasManyThroughRelation<TParent, TFar, LocalKey, FirstKey, SecondLocalKey, SecondKey>): Map<TParent[LocalKey], TFar[]>;
100
120
  declare function indexMorphToRelation<TChild, TParent extends object, MorphTypeKey extends keyof TChild & string, MorphIdKey extends keyof TChild & string, _OwnerKey extends keyof TParent & string = keyof TParent & string>(children: readonly TChild[], parentsByType: ReadonlyMap<string, ReadonlyMap<unknown, TParent>>, relation: MorphToRelation<TChild, MorphTypeKey, MorphIdKey>): Map<TChild[MorphIdKey], TParent>;
101
- export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation, };
102
- export { belongsTo, belongsToMany, getByRelationKey, hasMany, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasOneRelation, indexMorphManyRelation, indexMorphOneRelation, indexMorphToRelation, morphMany, morphOne, morphTo, relationMatchKey, };
121
+ export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation, };
122
+ export { belongsTo, belongsToMany, getByRelationKey, hasMany, hasManyThrough, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasManyThroughRelation, indexHasOneRelation, indexMorphManyRelation, indexMorphOneRelation, indexMorphToRelation, morphMany, morphOne, morphTo, relationMatchKey, };
@@ -1,6 +1,6 @@
1
1
  import type { PaginatedResult } from "../pagination/index.ts";
2
2
  import type BaseRepository from "./baseRepository.ts";
3
- import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, MorphManyRelation, MorphOneRelation, MorphToRelation } from "./relationships.ts";
3
+ import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, MorphManyRelation, MorphOneRelation, MorphToRelation } from "./relationships.ts";
4
4
  import type { QueryOptions, QueryWhere } from "./types.ts";
5
5
  import { WhereBuilder } from "./whereBuilder.ts";
6
6
  type LoadedRow = Record<string, unknown>;
@@ -31,6 +31,9 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
31
31
  withMorphOne<TChild extends object, LocalKey extends keyof TEntity & string, MorphTypeKey extends keyof TChild & string, MorphIdKey extends keyof TChild & string, Alias extends string>(as: Alias, relation: MorphOneRelation<TEntity, TChild, LocalKey, MorphTypeKey, MorphIdKey>, childRepository: BaseRepository<TChild, keyof TChild & string>, options?: Omit<QueryOptions<TChild>, "where">): this;
32
32
  withMorphTo<TParent extends object, MorphTypeKey extends keyof TEntity & string, MorphIdKey extends keyof TEntity & string, Alias extends string>(as: Alias, relation: MorphToRelation<TEntity, MorphTypeKey, MorphIdKey>, repositoriesByType: ReadonlyMap<string, BaseRepository<TParent, keyof TParent & string>>, options?: Omit<QueryOptions<TParent>, "where">): this;
33
33
  withBelongsToMany<TRelated extends object, Pivot extends object, ParentKey extends keyof TEntity & string, RelatedKey extends keyof TRelated & string, ForeignPivotKey extends keyof Pivot & string, RelatedPivotKey extends keyof Pivot & string, Alias extends string>(as: Alias, relation: BelongsToManyRelation<TEntity, TRelated, Pivot, ParentKey, RelatedKey, ForeignPivotKey, RelatedPivotKey>, relatedRepository: BaseRepository<TRelated, RelatedKey>, options?: Omit<QueryOptions<TRelated>, "where">): this;
34
+ withHasManyThrough<TFar extends object, LocalKey extends keyof TEntity & string, SecondKey extends keyof TFar & string, Alias extends string>(as: Alias, relation: HasManyThroughRelation<TEntity, TFar, LocalKey, string, string, SecondKey>, farRepository: BaseRepository<TFar, keyof TFar & string>, options?: Omit<QueryOptions<TFar>, "where">): this;
35
+ withTrashed(): this;
36
+ onlyTrashed(): this;
34
37
  get(): Promise<Array<TEntity & LoadedRow>>;
35
38
  first(): Promise<(TEntity & LoadedRow) | null>;
36
39
  count(): Promise<number>;
@@ -775,7 +775,8 @@ class BelongsToManyRelationQuery {
775
775
  return;
776
776
  }
777
777
  const list = Array.isArray(ids) ? ids : [ids];
778
- await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
778
+ const placeholders = list.map((_, index) => `$${index + 2}`).join(", ");
779
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} IN (${placeholders})`, [parentId, ...list]);
779
780
  }
780
781
  async sync(ids) {
781
782
  await this.detach();
@@ -957,6 +958,65 @@ class MorphToRelationQuery {
957
958
  }
958
959
  }
959
960
 
961
+ class HasManyThroughRelationQuery {
962
+ parent;
963
+ related;
964
+ relation;
965
+ kind = "hasManyThrough";
966
+ extraWhere = {};
967
+ extraOptions = {};
968
+ constructor(parent, related, relation) {
969
+ this.parent = parent;
970
+ this.related = related;
971
+ this.relation = relation;
972
+ }
973
+ where(where) {
974
+ this.extraWhere = { ...this.extraWhere, ...where };
975
+ return this;
976
+ }
977
+ orderBy(orderBy) {
978
+ this.extraOptions = { ...this.extraOptions, orderBy };
979
+ return this;
980
+ }
981
+ limit(limit) {
982
+ this.extraOptions = { ...this.extraOptions, limit };
983
+ return this;
984
+ }
985
+ applyEagerLoad(query, alias) {
986
+ query.withHasManyThrough(alias, this.relation, this.related.repository(), this.extraOptions);
987
+ }
988
+ hydrateEager(row, alias) {
989
+ const value = row[alias] ?? [];
990
+ const rows = Array.isArray(value) ? value : [];
991
+ return rows.map((item) => this.related.newFromRecord(item));
992
+ }
993
+ toExistsClause(parentTable) {
994
+ const farTable = this.related.repository().getTable().name;
995
+ const extra = buildAdvancedWhereClause(farTable, this.extraWhere, [], []);
996
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
997
+ const sql = `SELECT 1 FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(this.relation.throughTable)} ON ${qualifyColumn(this.relation.throughTable, this.relation.secondLocalKey)} = ${qualifyColumn(farTable, this.relation.secondKey)} WHERE ${qualifyColumn(this.relation.throughTable, this.relation.firstKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
998
+ return { sql, params: extra.params };
999
+ }
1000
+ async get() {
1001
+ const rows = await this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
1002
+ ...this.extraOptions,
1003
+ where: this.extraWhere
1004
+ });
1005
+ return rows.map((row) => this.related.newFromRecord(row));
1006
+ }
1007
+ async first() {
1008
+ const rows = await this.limit(1).get();
1009
+ return rows[0] ?? null;
1010
+ }
1011
+ async count() {
1012
+ const rows = await this.get();
1013
+ return rows.length;
1014
+ }
1015
+ then(onfulfilled, onrejected) {
1016
+ return thenGet(() => this.get(), onfulfilled, onrejected);
1017
+ }
1018
+ }
1019
+
960
1020
  // ../../src/core/database/relationships.ts
961
1021
  function hasMany(definition) {
962
1022
  return {
@@ -976,6 +1036,13 @@ function belongsTo(definition) {
976
1036
  ...definition
977
1037
  };
978
1038
  }
1039
+ function hasManyThrough(definition) {
1040
+ return {
1041
+ type: "hasManyThrough",
1042
+ throughParentKey: "__through_parent_id",
1043
+ ...definition
1044
+ };
1045
+ }
979
1046
  function belongsToMany(definition) {
980
1047
  return {
981
1048
  type: "belongsToMany",
@@ -1149,6 +1216,26 @@ function indexMorphOneRelation(parents, children, relation) {
1149
1216
  }
1150
1217
  return result;
1151
1218
  }
1219
+ function indexHasManyThroughRelation(parents, children, relation) {
1220
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
1221
+ const grouped = new Map;
1222
+ for (const child of children) {
1223
+ const key = relationMatchKey(child[throughKey]);
1224
+ if (key === "") {
1225
+ continue;
1226
+ }
1227
+ const existing = grouped.get(key) ?? [];
1228
+ const { [throughKey]: _through, ...far } = child;
1229
+ existing.push(far);
1230
+ grouped.set(key, existing);
1231
+ }
1232
+ const result = new Map;
1233
+ for (const parent of parents) {
1234
+ const key = relationMatchKey(parent[relation.localKey]);
1235
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
1236
+ }
1237
+ return result;
1238
+ }
1152
1239
  function indexMorphToRelation(children, parentsByType, relation) {
1153
1240
  const result = new Map;
1154
1241
  for (const child of children) {
@@ -1493,6 +1580,18 @@ class ModelQuery {
1493
1580
  this.query.withMorphTo(...args);
1494
1581
  return this;
1495
1582
  }
1583
+ withHasManyThrough(...args) {
1584
+ this.query.withHasManyThrough(...args);
1585
+ return this;
1586
+ }
1587
+ withTrashed() {
1588
+ this.query.withTrashed();
1589
+ return this;
1590
+ }
1591
+ onlyTrashed() {
1592
+ this.query.onlyTrashed();
1593
+ return this;
1594
+ }
1496
1595
  async get() {
1497
1596
  const statics = modelStatics(this.modelClass);
1498
1597
  const rows = await this.query.get();
@@ -1700,6 +1799,31 @@ class Model {
1700
1799
  static with(...relations) {
1701
1800
  return Model.query.call(this).with(...relations);
1702
1801
  }
1802
+ static withTrashed() {
1803
+ return Model.query.call(this).withTrashed();
1804
+ }
1805
+ static onlyTrashed() {
1806
+ return Model.query.call(this).onlyTrashed();
1807
+ }
1808
+ static async chunk(count, callback) {
1809
+ const statics = modelStatics(this);
1810
+ ensureBooted(this);
1811
+ await resolveModelRepository(this).chunk(count, async (rows) => {
1812
+ return await callback(rows.map((row) => statics.newFromRecord(row, true)));
1813
+ });
1814
+ }
1815
+ static async cursorPaginate(options) {
1816
+ const statics = modelStatics(this);
1817
+ ensureBooted(this);
1818
+ const page = await resolveModelRepository(this).cursorPaginate({
1819
+ perPage: options.perPage,
1820
+ cursor: options.cursor
1821
+ });
1822
+ return {
1823
+ data: page.data.map((row) => statics.newFromRecord(row, true)),
1824
+ meta: page.meta
1825
+ };
1826
+ }
1703
1827
  static whereHas(name, constrain) {
1704
1828
  return Model.query.call(this).whereHas(name, constrain);
1705
1829
  }
@@ -1884,6 +2008,20 @@ class Model {
1884
2008
  ownerKey: ownerKey ?? relatedTable.primaryKey
1885
2009
  }));
1886
2010
  }
2011
+ hasManyThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
2012
+ const table = this.repository.getTable();
2013
+ const relatedClass = resolveRelated(related);
2014
+ const throughClass = resolveRelated(through);
2015
+ const throughTable = throughClass.repository().getTable();
2016
+ return new HasManyThroughRelationQuery(this, relatedClass, hasManyThrough({
2017
+ name: relatedClass.repository().getTable().name,
2018
+ throughTable: throughTable.name,
2019
+ localKey: localKey ?? table.primaryKey,
2020
+ firstKey: firstKey ?? foreignKeyFromTable(table.name),
2021
+ secondLocalKey: secondLocalKey ?? throughTable.primaryKey,
2022
+ secondKey: secondKey ?? foreignKeyFromTable(throughTable.name)
2023
+ }));
2024
+ }
1887
2025
  belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
1888
2026
  const table = this.repository.getTable();
1889
2027
  const relatedClass = resolveRelated(related);
@@ -1975,6 +2113,7 @@ export {
1975
2113
  BelongsToManyRelationQuery,
1976
2114
  BelongsToRelationQuery,
1977
2115
  HasManyRelationQuery,
2116
+ HasManyThroughRelationQuery,
1978
2117
  HasOneRelationQuery,
1979
2118
  Model,
1980
2119
  ModelQuery,
@@ -18,6 +18,13 @@ function belongsTo(definition) {
18
18
  ...definition
19
19
  };
20
20
  }
21
+ function hasManyThrough(definition) {
22
+ return {
23
+ type: "hasManyThrough",
24
+ throughParentKey: "__through_parent_id",
25
+ ...definition
26
+ };
27
+ }
21
28
  function belongsToMany(definition) {
22
29
  return {
23
30
  type: "belongsToMany",
@@ -191,6 +198,26 @@ function indexMorphOneRelation(parents, children, relation) {
191
198
  }
192
199
  return result;
193
200
  }
201
+ function indexHasManyThroughRelation(parents, children, relation) {
202
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
203
+ const grouped = new Map;
204
+ for (const child of children) {
205
+ const key = relationMatchKey(child[throughKey]);
206
+ if (key === "") {
207
+ continue;
208
+ }
209
+ const existing = grouped.get(key) ?? [];
210
+ const { [throughKey]: _through, ...far } = child;
211
+ existing.push(far);
212
+ grouped.set(key, existing);
213
+ }
214
+ const result = new Map;
215
+ for (const parent of parents) {
216
+ const key = relationMatchKey(parent[relation.localKey]);
217
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
218
+ }
219
+ return result;
220
+ }
194
221
  function indexMorphToRelation(children, parentsByType, relation) {
195
222
  const result = new Map;
196
223
  for (const child of children) {
@@ -211,10 +238,12 @@ export {
211
238
  belongsToMany,
212
239
  getByRelationKey,
213
240
  hasMany,
241
+ hasManyThrough,
214
242
  hasOne,
215
243
  indexBelongsToManyRelation,
216
244
  indexBelongsToRelation,
217
245
  indexHasManyRelation,
246
+ indexHasManyThroughRelation,
218
247
  indexHasOneRelation,
219
248
  indexMorphManyRelation,
220
249
  indexMorphOneRelation,
@@ -441,6 +441,13 @@ function belongsTo(definition) {
441
441
  ...definition
442
442
  };
443
443
  }
444
+ function hasManyThrough(definition) {
445
+ return {
446
+ type: "hasManyThrough",
447
+ throughParentKey: "__through_parent_id",
448
+ ...definition
449
+ };
450
+ }
444
451
  function belongsToMany(definition) {
445
452
  return {
446
453
  type: "belongsToMany",
@@ -614,6 +621,26 @@ function indexMorphOneRelation(parents, children, relation) {
614
621
  }
615
622
  return result;
616
623
  }
624
+ function indexHasManyThroughRelation(parents, children, relation) {
625
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
626
+ const grouped = new Map;
627
+ for (const child of children) {
628
+ const key = relationMatchKey(child[throughKey]);
629
+ if (key === "") {
630
+ continue;
631
+ }
632
+ const existing = grouped.get(key) ?? [];
633
+ const { [throughKey]: _through, ...far } = child;
634
+ existing.push(far);
635
+ grouped.set(key, existing);
636
+ }
637
+ const result = new Map;
638
+ for (const parent of parents) {
639
+ const key = relationMatchKey(parent[relation.localKey]);
640
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
641
+ }
642
+ return result;
643
+ }
617
644
  function indexMorphToRelation(children, parentsByType, relation) {
618
645
  const result = new Map;
619
646
  for (const child of children) {
@@ -797,6 +824,24 @@ class RepositoryQuery {
797
824
  });
798
825
  return this;
799
826
  }
827
+ withHasManyThrough(as, relation, farRepository, options = {}) {
828
+ this.eagerLoads.push({
829
+ kind: "hasManyThrough",
830
+ as,
831
+ relation,
832
+ repository: farRepository,
833
+ options
834
+ });
835
+ return this;
836
+ }
837
+ withTrashed() {
838
+ this.queryOptions = { ...this.queryOptions, withTrashed: true };
839
+ return this;
840
+ }
841
+ onlyTrashed() {
842
+ this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
843
+ return this;
844
+ }
800
845
  async get() {
801
846
  const rows = await this.repository.findAll(this.buildOptions());
802
847
  return await this.attach(rows);
@@ -882,6 +927,15 @@ class RepositoryQuery {
882
927
  }));
883
928
  continue;
884
929
  }
930
+ if (load.kind === "hasManyThrough") {
931
+ const relation2 = load.relation;
932
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
933
+ result = result.map((row) => ({
934
+ ...row,
935
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
936
+ }));
937
+ continue;
938
+ }
885
939
  if (load.kind === "belongsToMany") {
886
940
  const relation2 = load.relation;
887
941
  const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
@@ -94,10 +94,30 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
94
94
  "POST /billing/webhooks/stripe": "Stripe webhook receiver (stub)",
95
95
  "GET /scim/v2/Users": "SCIM list users",
96
96
  "POST /scim/v2/Users": "SCIM create user",
97
- "GET /scim/v2/Groups": "SCIM list groups (organizations)",
98
97
  "GET /health": "Liveness probe",
99
98
  "GET /ready": "Readiness probe",
100
- "GET /metrics": "Prometheus metrics"
99
+ "GET /metrics": "Prometheus metrics",
100
+ "GET /api/user": "Current authenticated HiroApp user",
101
+ "POST /api/login": "Login with email and password",
102
+ "POST /api/logout": "Log out the current cookie session",
103
+ "POST /api/auth/two-factor-challenge": "Complete staff two-factor login challenge",
104
+ "GET /api/users/me": "Current user profile",
105
+ "PATCH /api/users/me": "Update profile name and email",
106
+ "PUT /api/users/me/password": "Change password",
107
+ "GET /api/users/me/sessions": "List cookie browser sessions",
108
+ "DELETE /api/users/me/sessions/:id": "Revoke one cookie browser session",
109
+ "GET /api/auth/tokens": "List API tokens",
110
+ "POST /api/auth/tokens": "Create API token",
111
+ "DELETE /api/auth/tokens/:id": "Revoke API token",
112
+ "GET /api/applications": "List applications",
113
+ "POST /api/applications": "Submit an application",
114
+ "GET /api/positions": "List hiring positions",
115
+ "GET /api/departments": "List departments",
116
+ "GET /api/webhooks": "List hiring webhooks",
117
+ "POST /api/webhooks": "Create a hiring webhook",
118
+ "GET /api/audit-logs": "List hiring audit log entries",
119
+ "GET /api/billing/subscription": "Current tenant subscription",
120
+ "GET /scim/v2/Groups": "SCIM list groups (departments)"
101
121
  };
102
122
  function toOpenApiPath(path) {
103
123
  return path.replace(/:([A-Za-z_]+)/g, "{$1}");
package/dist/index.js CHANGED
@@ -2078,6 +2078,13 @@ function belongsTo(definition) {
2078
2078
  ...definition
2079
2079
  };
2080
2080
  }
2081
+ function hasManyThrough(definition) {
2082
+ return {
2083
+ type: "hasManyThrough",
2084
+ throughParentKey: "__through_parent_id",
2085
+ ...definition
2086
+ };
2087
+ }
2081
2088
  function belongsToMany(definition) {
2082
2089
  return {
2083
2090
  type: "belongsToMany",
@@ -2251,6 +2258,26 @@ function indexMorphOneRelation(parents, children, relation) {
2251
2258
  }
2252
2259
  return result;
2253
2260
  }
2261
+ function indexHasManyThroughRelation(parents, children, relation) {
2262
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
2263
+ const grouped = new Map;
2264
+ for (const child of children) {
2265
+ const key = relationMatchKey(child[throughKey]);
2266
+ if (key === "") {
2267
+ continue;
2268
+ }
2269
+ const existing = grouped.get(key) ?? [];
2270
+ const { [throughKey]: _through, ...far } = child;
2271
+ existing.push(far);
2272
+ grouped.set(key, existing);
2273
+ }
2274
+ const result = new Map;
2275
+ for (const parent of parents) {
2276
+ const key = relationMatchKey(parent[relation.localKey]);
2277
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
2278
+ }
2279
+ return result;
2280
+ }
2254
2281
  function indexMorphToRelation(children, parentsByType, relation) {
2255
2282
  const result = new Map;
2256
2283
  for (const child of children) {
@@ -2434,6 +2461,24 @@ class RepositoryQuery {
2434
2461
  });
2435
2462
  return this;
2436
2463
  }
2464
+ withHasManyThrough(as, relation, farRepository, options = {}) {
2465
+ this.eagerLoads.push({
2466
+ kind: "hasManyThrough",
2467
+ as,
2468
+ relation,
2469
+ repository: farRepository,
2470
+ options
2471
+ });
2472
+ return this;
2473
+ }
2474
+ withTrashed() {
2475
+ this.queryOptions = { ...this.queryOptions, withTrashed: true };
2476
+ return this;
2477
+ }
2478
+ onlyTrashed() {
2479
+ this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
2480
+ return this;
2481
+ }
2437
2482
  async get() {
2438
2483
  const rows = await this.repository.findAll(this.buildOptions());
2439
2484
  return await this.attach(rows);
@@ -2519,6 +2564,15 @@ class RepositoryQuery {
2519
2564
  }));
2520
2565
  continue;
2521
2566
  }
2567
+ if (load.kind === "hasManyThrough") {
2568
+ const relation2 = load.relation;
2569
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
2570
+ result = result.map((row) => ({
2571
+ ...row,
2572
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
2573
+ }));
2574
+ continue;
2575
+ }
2522
2576
  if (load.kind === "belongsToMany") {
2523
2577
  const relation2 = load.relation;
2524
2578
  const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
@@ -2807,6 +2861,55 @@ class BaseRepository {
2807
2861
  }, options);
2808
2862
  return indexHasManyRelation(parents, children, relation);
2809
2863
  }
2864
+ async findHasManyThrough(parentId, relation, options = {}) {
2865
+ const grouped = await this.loadHasManyThroughForParents([{ [relation.localKey]: parentId }], relation, options);
2866
+ return getByRelationKey(grouped, parentId) ?? [];
2867
+ }
2868
+ async loadHasManyThroughForParents(parents, relation, options = {}) {
2869
+ if (parents.length === 0) {
2870
+ return indexHasManyThroughRelation(parents, [], relation);
2871
+ }
2872
+ const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
2873
+ const throughParentKey = relation.throughParentKey ?? "__through_parent_id";
2874
+ const farTable = this.table.name;
2875
+ const columns = this.table.columns.map((column) => `${qualifyColumn(farTable, column)}`).join(", ");
2876
+ const placeholders = parentIds.map((_, index) => `$${index + 1}`).join(", ");
2877
+ const { text: extraWhere, params: extraParams } = this.buildThroughWhere(options, parentIds.length);
2878
+ const softDelete = this.throughSoftDeleteClause(options);
2879
+ const sql = `SELECT ${columns}, ${qualifyColumn(relation.throughTable, relation.firstKey)} AS ${throughParentKey} FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(relation.throughTable)} ON ${qualifyColumn(relation.throughTable, relation.secondLocalKey)} = ${qualifyColumn(farTable, relation.secondKey)} WHERE ${qualifyColumn(relation.throughTable, relation.firstKey)} IN (${placeholders})${softDelete}${extraWhere}`;
2880
+ const children = await this.connection.unsafe(sql, [
2881
+ ...parentIds,
2882
+ ...extraParams
2883
+ ]);
2884
+ return indexHasManyThroughRelation(parents, children, relation);
2885
+ }
2886
+ throughSoftDeleteClause(options) {
2887
+ const column = resolveSoftDeleteColumn(this.table);
2888
+ if (!column) {
2889
+ return "";
2890
+ }
2891
+ const qualified = qualifyColumn(this.table.name, column);
2892
+ if (options.onlyTrashed) {
2893
+ return ` AND ${qualified} IS NOT NULL`;
2894
+ }
2895
+ if (options.withTrashed) {
2896
+ return "";
2897
+ }
2898
+ return ` AND ${qualified} IS NULL`;
2899
+ }
2900
+ buildThroughWhere(options, paramOffset = 1) {
2901
+ const where = options.where ?? {};
2902
+ const entries = Object.entries(where);
2903
+ if (entries.length === 0) {
2904
+ return { text: "", params: [] };
2905
+ }
2906
+ const params = [];
2907
+ const clauses = entries.map(([column, value], index) => {
2908
+ params.push(value);
2909
+ return `${qualifyColumn(this.table.name, column)} = $${paramOffset + index + 1}`;
2910
+ });
2911
+ return { text: ` AND ${clauses.join(" AND ")}`, params };
2912
+ }
2810
2913
  async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
2811
2914
  if (children.length === 0) {
2812
2915
  return new Map;
@@ -2872,7 +2975,8 @@ class BaseRepository {
2872
2975
  return indexBelongsToManyRelation(parents, [], [], relation);
2873
2976
  }
2874
2977
  const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
2875
- const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = ANY($1)`, [parentIds]);
2978
+ const placeholders = parentIds.map((_, index) => `$${index + 1}`).join(", ");
2979
+ const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} IN (${placeholders})`, parentIds);
2876
2980
  if (pivotRows.length === 0) {
2877
2981
  return indexBelongsToManyRelation(parents, [], [], relation);
2878
2982
  }
@@ -3534,7 +3638,8 @@ class BelongsToManyRelationQuery {
3534
3638
  return;
3535
3639
  }
3536
3640
  const list = Array.isArray(ids) ? ids : [ids];
3537
- await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
3641
+ const placeholders = list.map((_, index) => `$${index + 2}`).join(", ");
3642
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} IN (${placeholders})`, [parentId, ...list]);
3538
3643
  }
3539
3644
  async sync(ids) {
3540
3645
  await this.detach();
@@ -3716,6 +3821,65 @@ class MorphToRelationQuery {
3716
3821
  }
3717
3822
  }
3718
3823
 
3824
+ class HasManyThroughRelationQuery {
3825
+ parent;
3826
+ related;
3827
+ relation;
3828
+ kind = "hasManyThrough";
3829
+ extraWhere = {};
3830
+ extraOptions = {};
3831
+ constructor(parent, related, relation) {
3832
+ this.parent = parent;
3833
+ this.related = related;
3834
+ this.relation = relation;
3835
+ }
3836
+ where(where) {
3837
+ this.extraWhere = { ...this.extraWhere, ...where };
3838
+ return this;
3839
+ }
3840
+ orderBy(orderBy) {
3841
+ this.extraOptions = { ...this.extraOptions, orderBy };
3842
+ return this;
3843
+ }
3844
+ limit(limit) {
3845
+ this.extraOptions = { ...this.extraOptions, limit };
3846
+ return this;
3847
+ }
3848
+ applyEagerLoad(query, alias) {
3849
+ query.withHasManyThrough(alias, this.relation, this.related.repository(), this.extraOptions);
3850
+ }
3851
+ hydrateEager(row, alias) {
3852
+ const value = row[alias] ?? [];
3853
+ const rows = Array.isArray(value) ? value : [];
3854
+ return rows.map((item) => this.related.newFromRecord(item));
3855
+ }
3856
+ toExistsClause(parentTable) {
3857
+ const farTable = this.related.repository().getTable().name;
3858
+ const extra = buildAdvancedWhereClause(farTable, this.extraWhere, [], []);
3859
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3860
+ const sql = `SELECT 1 FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(this.relation.throughTable)} ON ${qualifyColumn(this.relation.throughTable, this.relation.secondLocalKey)} = ${qualifyColumn(farTable, this.relation.secondKey)} WHERE ${qualifyColumn(this.relation.throughTable, this.relation.firstKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3861
+ return { sql, params: extra.params };
3862
+ }
3863
+ async get() {
3864
+ const rows = await this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
3865
+ ...this.extraOptions,
3866
+ where: this.extraWhere
3867
+ });
3868
+ return rows.map((row) => this.related.newFromRecord(row));
3869
+ }
3870
+ async first() {
3871
+ const rows = await this.limit(1).get();
3872
+ return rows[0] ?? null;
3873
+ }
3874
+ async count() {
3875
+ const rows = await this.get();
3876
+ return rows.length;
3877
+ }
3878
+ then(onfulfilled, onrejected) {
3879
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3880
+ }
3881
+ }
3882
+
3719
3883
  // ../../src/core/database/model.ts
3720
3884
  var modelRepositories = new WeakMap;
3721
3885
  var namedModels = new Map;
@@ -4044,6 +4208,18 @@ class ModelQuery {
4044
4208
  this.query.withMorphTo(...args);
4045
4209
  return this;
4046
4210
  }
4211
+ withHasManyThrough(...args) {
4212
+ this.query.withHasManyThrough(...args);
4213
+ return this;
4214
+ }
4215
+ withTrashed() {
4216
+ this.query.withTrashed();
4217
+ return this;
4218
+ }
4219
+ onlyTrashed() {
4220
+ this.query.onlyTrashed();
4221
+ return this;
4222
+ }
4047
4223
  async get() {
4048
4224
  const statics = modelStatics(this.modelClass);
4049
4225
  const rows = await this.query.get();
@@ -4251,6 +4427,31 @@ class Model {
4251
4427
  static with(...relations) {
4252
4428
  return Model.query.call(this).with(...relations);
4253
4429
  }
4430
+ static withTrashed() {
4431
+ return Model.query.call(this).withTrashed();
4432
+ }
4433
+ static onlyTrashed() {
4434
+ return Model.query.call(this).onlyTrashed();
4435
+ }
4436
+ static async chunk(count, callback) {
4437
+ const statics = modelStatics(this);
4438
+ ensureBooted(this);
4439
+ await resolveModelRepository(this).chunk(count, async (rows) => {
4440
+ return await callback(rows.map((row) => statics.newFromRecord(row, true)));
4441
+ });
4442
+ }
4443
+ static async cursorPaginate(options) {
4444
+ const statics = modelStatics(this);
4445
+ ensureBooted(this);
4446
+ const page = await resolveModelRepository(this).cursorPaginate({
4447
+ perPage: options.perPage,
4448
+ cursor: options.cursor
4449
+ });
4450
+ return {
4451
+ data: page.data.map((row) => statics.newFromRecord(row, true)),
4452
+ meta: page.meta
4453
+ };
4454
+ }
4254
4455
  static whereHas(name, constrain) {
4255
4456
  return Model.query.call(this).whereHas(name, constrain);
4256
4457
  }
@@ -4435,6 +4636,20 @@ class Model {
4435
4636
  ownerKey: ownerKey ?? relatedTable.primaryKey
4436
4637
  }));
4437
4638
  }
4639
+ hasManyThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
4640
+ const table = this.repository.getTable();
4641
+ const relatedClass = resolveRelated(related);
4642
+ const throughClass = resolveRelated(through);
4643
+ const throughTable = throughClass.repository().getTable();
4644
+ return new HasManyThroughRelationQuery(this, relatedClass, hasManyThrough({
4645
+ name: relatedClass.repository().getTable().name,
4646
+ throughTable: throughTable.name,
4647
+ localKey: localKey ?? table.primaryKey,
4648
+ firstKey: firstKey ?? foreignKeyFromTable(table.name),
4649
+ secondLocalKey: secondLocalKey ?? throughTable.primaryKey,
4650
+ secondKey: secondKey ?? foreignKeyFromTable(throughTable.name)
4651
+ }));
4652
+ }
4438
4653
  belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
4439
4654
  const table = this.repository.getTable();
4440
4655
  const relatedClass = resolveRelated(related);
@@ -8076,17 +8291,33 @@ async function resolveTenantForRequest(request) {
8076
8291
  if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
8077
8292
  throw new ForbiddenError("Tenant header does not match your account.");
8078
8293
  }
8079
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
8294
+ const memberTenant = await resolveTenant(userTenantId);
8295
+ if (memberTenant) {
8296
+ return memberTenant;
8297
+ }
8298
+ return DEFAULT_TENANT;
8080
8299
  }
8081
8300
  if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
8082
- return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
8301
+ const headerTenant = await resolveTenant(parsedHeader);
8302
+ if (headerTenant) {
8303
+ return headerTenant;
8304
+ }
8305
+ return DEFAULT_TENANT;
8306
+ }
8307
+ const adminTenant = await resolveTenant(userTenantId);
8308
+ if (adminTenant) {
8309
+ return adminTenant;
8083
8310
  }
8084
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
8311
+ return DEFAULT_TENANT;
8085
8312
  }
8086
8313
  }
8087
8314
  const headerTenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : null;
8088
8315
  const tenantId = isPublicReadsEnabled() && headerTenantId !== null ? headerTenantId : DEFAULT_TENANT.id;
8089
- return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
8316
+ const guestTenant = await resolveTenant(tenantId);
8317
+ if (guestTenant) {
8318
+ return guestTenant;
8319
+ }
8320
+ return DEFAULT_TENANT;
8090
8321
  }
8091
8322
  function createTenantMiddleware() {
8092
8323
  return async (request, next) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.100",
3
+ "version": "0.5.101",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",