@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 +4 -0
- package/dist/core/database/baseRepository.d.ts +5 -1
- package/dist/core/database/index.d.ts +3 -3
- package/dist/core/database/model.d.ts +21 -2
- package/dist/core/database/relationQuery.d.ts +22 -3
- package/dist/core/database/relationships.d.ts +22 -2
- package/dist/core/database/repositoryQuery.d.ts +4 -1
- package/dist/entries/database/model.js +140 -1
- package/dist/entries/database/relationships.js +29 -0
- package/dist/entries/database/repositoryQuery.js +54 -0
- package/dist/entries/openapi/generator.js +22 -2
- package/dist/index.js +237 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) => {
|