Skip to content

Commit cba5a4b

Browse files
committed
feat!: Add sql template and loader method
1 parent ceba08f commit cba5a4b

7 files changed

Lines changed: 1082 additions & 0 deletions

File tree

packages/entity-database-adapter-knex/src/AuthorizationResultBasedKnexEntityLoader.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
QuerySelectionModifiers,
1515
QuerySelectionModifiersWithOrderByRaw,
1616
} from './BasePostgresEntityDatabaseAdapter';
17+
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder';
18+
import { SQLFragment } from './SQLOperator';
1719
import { EntityKnexDataManager } from './internal/EntityKnexDataManager';
1820

1921
/**
@@ -108,4 +110,90 @@ export class AuthorizationResultBasedKnexEntityLoader<
108110
);
109111
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
110112
}
113+
114+
/**
115+
* Create a SQL query builder for this loader using Drizzle-style SQL operators.
116+
*
117+
* @example
118+
* ```ts
119+
* import { sql, sqlHelpers } from '@expo/entity-database-adapter-knex';
120+
*
121+
* const results = await loader
122+
* .loadManyBySQL(sql`age >= ${18} AND status = ${'active'}`)
123+
* .orderBy('createdAt', 'DESC')
124+
* .limit(10)
125+
* .executeAsync();
126+
*
127+
* const { between, inArray } = sqlHelpers;
128+
* const filtered = await loader
129+
* .loadManyBySQL(
130+
* sql`${between('age', 18, 65)} AND ${inArray('role', ['admin', 'moderator'])}`
131+
* )
132+
* .executeAsync();
133+
* ```
134+
*/
135+
loadManyBySQL(
136+
fragment: SQLFragment,
137+
): AuthorizationResultBasedSQLQueryBuilder<
138+
TFields,
139+
TIDField,
140+
TViewerContext,
141+
TEntity,
142+
TPrivacyPolicy,
143+
TSelectedFields
144+
> {
145+
return new AuthorizationResultBasedSQLQueryBuilder(this, fragment);
146+
}
147+
}
148+
149+
/**
150+
* SQL query builder for AuthorizationResultBasedKnexEntityLoader.
151+
* Provides a fluent API for building and executing SQL queries with authorization.
152+
*/
153+
export class AuthorizationResultBasedSQLQueryBuilder<
154+
TFields extends Record<string, any>,
155+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
156+
TViewerContext extends ViewerContext,
157+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
158+
TPrivacyPolicy extends EntityPrivacyPolicy<
159+
TFields,
160+
TIDField,
161+
TViewerContext,
162+
TEntity,
163+
TSelectedFields
164+
>,
165+
TSelectedFields extends keyof TFields,
166+
> extends BaseSQLQueryBuilder<TFields, Result<TEntity>> {
167+
constructor(
168+
private readonly loader: AuthorizationResultBasedKnexEntityLoader<
169+
TFields,
170+
TIDField,
171+
TViewerContext,
172+
TEntity,
173+
TPrivacyPolicy,
174+
TSelectedFields
175+
>,
176+
sqlFragment: SQLFragment,
177+
) {
178+
super(sqlFragment);
179+
}
180+
181+
/**
182+
* Execute the query and return results with authorization
183+
*/
184+
async executeAsync(): Promise<readonly Result<TEntity>[]> {
185+
return await this.loader.loadManyByRawWhereClauseAsync(
186+
this.getSQLFragment().sql,
187+
this.getSQLFragment().values,
188+
this.getModifiers(),
189+
);
190+
}
191+
192+
/**
193+
* Execute the query and return the first result or null
194+
*/
195+
override async executeFirstAsync(): Promise<Result<TEntity> | null> {
196+
const results = await this.limit(1).executeAsync();
197+
return results[0] ?? null;
198+
}
111199
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
OrderByOrdering,
3+
QuerySelectionModifiersWithOrderByRaw,
4+
} from './BasePostgresEntityDatabaseAdapter';
5+
import { SQLFragment } from './SQLOperator';
6+
7+
/**
8+
* Base SQL query builder that provides common functionality for building SQL queries.
9+
* This class is extended by specific implementations that handle authorization differently.
10+
*/
11+
export abstract class BaseSQLQueryBuilder<TFields extends Record<string, any>, TResultType> {
12+
protected readonly modifiers: {
13+
limit?: number;
14+
offset?: number;
15+
orderBy?: { fieldName: keyof TFields; order: OrderByOrdering }[];
16+
orderByRaw?: string;
17+
} = {};
18+
19+
constructor(protected readonly sqlFragment: SQLFragment) {}
20+
21+
/**
22+
* Limit the number of results
23+
*/
24+
limit(n: number): this {
25+
this.modifiers.limit = n;
26+
return this;
27+
}
28+
29+
/**
30+
* Skip a number of results
31+
*/
32+
offset(n: number): this {
33+
this.modifiers.offset = n;
34+
return this;
35+
}
36+
37+
/**
38+
* Order by a field
39+
*/
40+
orderBy(fieldName: keyof TFields, order: 'ASC' | 'DESC' = 'ASC'): this {
41+
const orderByOrdering =
42+
order === 'ASC' ? OrderByOrdering.ASCENDING : OrderByOrdering.DESCENDING;
43+
this.modifiers.orderBy = [
44+
...(this.modifiers.orderBy ?? []),
45+
{ fieldName, order: orderByOrdering },
46+
];
47+
return this;
48+
}
49+
50+
/**
51+
* Order by a raw SQL expression
52+
*/
53+
orderByRaw(sql: string): this {
54+
this.modifiers.orderByRaw = sql;
55+
return this;
56+
}
57+
58+
/**
59+
* Get the current modifiers as QuerySelectionModifiersWithOrderByRaw
60+
*/
61+
protected getModifiers(): QuerySelectionModifiersWithOrderByRaw<TFields> {
62+
return this.modifiers;
63+
}
64+
65+
/**
66+
* Get the SQL fragment
67+
*/
68+
protected getSQLFragment(): SQLFragment {
69+
return this.sqlFragment;
70+
}
71+
72+
/**
73+
* Execute the query and return results.
74+
* Implementation depends on the specific loader type.
75+
*/
76+
abstract executeAsync(): Promise<readonly TResultType[]>;
77+
78+
/**
79+
* Execute the query and return the first result or null.
80+
* Default implementation uses executeAsync with limit(1).
81+
*/
82+
async executeFirstAsync(): Promise<TResultType | null> {
83+
const results = await this.limit(1).executeAsync();
84+
return Array.isArray(results) && results.length > 0 ? results[0] : null;
85+
}
86+
}

packages/entity-database-adapter-knex/src/EnforcingKnexEntityLoader.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
QuerySelectionModifiers,
77
QuerySelectionModifiersWithOrderByRaw,
88
} from './BasePostgresEntityDatabaseAdapter';
9+
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder';
10+
import { SQLFragment } from './SQLOperator';
911

1012
/**
1113
* Enforcing knex entity loader for non-data-loader-based load methods.
@@ -122,4 +124,93 @@ export class EnforcingKnexEntityLoader<
122124
);
123125
return entityResults.map((result) => result.enforceValue());
124126
}
127+
128+
/**
129+
* Create a SQL query builder for this loader using Drizzle-style SQL operators.
130+
* All queries will enforce authorization and throw if not authorized.
131+
*
132+
* @example
133+
* ```ts
134+
* import { sql, sqlHelpers } from '@expo/entity-database-adapter-knex';
135+
*
136+
* const users = await loader
137+
* .loadManyBySQL(sql`age >= ${18} AND status = ${'active'}`)
138+
* .orderBy('createdAt', 'DESC')
139+
* .limit(10)
140+
* .executeAsync();
141+
*
142+
* const { between, inArray } = sqlHelpers;
143+
* const filtered = await loader
144+
* .loadManyBySQL(
145+
* sql`${between('age', 18, 65)} AND ${inArray('role', ['admin', 'moderator'])}`
146+
* )
147+
* .executeAsync();
148+
* ```
149+
*/
150+
loadManyBySQL(
151+
fragment: SQLFragment,
152+
): EnforcingSQLQueryBuilder<
153+
TFields,
154+
TIDField,
155+
TViewerContext,
156+
TEntity,
157+
TPrivacyPolicy,
158+
TSelectedFields
159+
> {
160+
return new EnforcingSQLQueryBuilder(this, fragment);
161+
}
162+
}
163+
164+
/**
165+
* SQL query builder for EnforcingKnexEntityLoader.
166+
* Provides a fluent API for building and executing SQL queries with enforced authorization.
167+
*/
168+
export class EnforcingSQLQueryBuilder<
169+
TFields extends Record<string, any>,
170+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
171+
TViewerContext extends ViewerContext,
172+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
173+
TPrivacyPolicy extends EntityPrivacyPolicy<
174+
TFields,
175+
TIDField,
176+
TViewerContext,
177+
TEntity,
178+
TSelectedFields
179+
>,
180+
TSelectedFields extends keyof TFields,
181+
> extends BaseSQLQueryBuilder<TFields, TEntity> {
182+
constructor(
183+
private readonly loader: EnforcingKnexEntityLoader<
184+
TFields,
185+
TIDField,
186+
TViewerContext,
187+
TEntity,
188+
TPrivacyPolicy,
189+
TSelectedFields
190+
>,
191+
sqlFragment: SQLFragment,
192+
) {
193+
super(sqlFragment);
194+
}
195+
196+
/**
197+
* Execute the query and return results with enforced authorization
198+
* @throws EntityNotAuthorizedError if viewer is not authorized to view any entity
199+
*/
200+
async executeAsync(): Promise<readonly TEntity[]> {
201+
return await this.loader.loadManyByRawWhereClauseAsync(
202+
this.getSQLFragment().sql,
203+
this.getSQLFragment().values,
204+
this.getModifiers(),
205+
);
206+
}
207+
208+
/**
209+
* Execute the query and return the first result or null
210+
* @throws EntityNotAuthorizedError if viewer is not authorized to view the entity
211+
*/
212+
override async executeFirstAsync(): Promise<TEntity | null> {
213+
const results = await this.limit(1).executeAsync();
214+
return results[0] ?? null;
215+
}
125216
}

0 commit comments

Comments
 (0)