Skip to content

Commit b2ec0d7

Browse files
committed
Create implementation of DtoRepo for EdgeDB
1 parent 486889a commit b2ec0d7

File tree

3 files changed

+257
-1
lines changed

3 files changed

+257
-1
lines changed

src/core/database/dto.repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { DbTypeOf } from './db-type';
1818
import { OnIndex } from './indexer';
1919
import { matchProps } from './query';
2020

21-
export const privileges = Symbol('DtoRepository.privileges');
21+
export const privileges = Symbol.for('DtoRepository.privileges');
2222

2323
/**
2424
* A repository for a simple DTO. This provides a few methods out of the box.

src/core/edgedb/dto.repository.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import {
3+
isNotFalsy,
4+
many,
5+
Many,
6+
mapKeys,
7+
Nil,
8+
setOf,
9+
} from '@seedcompany/common';
10+
import { LazyGetter as Once } from 'lazy-get-decorator';
11+
import { lowerCase } from 'lodash';
12+
import { AbstractClass } from 'type-fest';
13+
import {
14+
EnhancedResource,
15+
ID,
16+
NotFoundException,
17+
PaginatedListType,
18+
PaginationInput,
19+
ResourceShape,
20+
SortablePaginationInput,
21+
} from '~/common';
22+
import { ResourceLike } from '~/core';
23+
import { Privileges } from '../../components/authorization';
24+
import { getChanges } from '../database/changes';
25+
import { privileges } from '../database/dto.repository';
26+
import { CommonRepository } from './common.repository';
27+
import { InsertShape } from './generated-client/insert';
28+
import { $expr_PathNode, $linkPropify } from './generated-client/path';
29+
import {
30+
$expr_Select,
31+
objectTypeToSelectShape,
32+
SelectFilterExpression,
33+
SelectModifiers,
34+
} from './generated-client/select';
35+
import { UpdateShape } from './generated-client/update';
36+
import { $, e } from './reexports';
37+
38+
/**
39+
* A repository for a simple DTO. This provides a few methods out of the box.
40+
*/
41+
export const RepoFor = <
42+
TResourceStatic extends ResourceShape<any>,
43+
DBType extends (TResourceStatic['DB'] & {})['__element__'],
44+
HydratedShape extends objectTypeToSelectShape<DBType> = (TResourceStatic['DB'] & {})['*'],
45+
>(
46+
resourceIn: TResourceStatic,
47+
options: {
48+
hydrate?: ShapeFn<$.TypeSet<DBType>, HydratedShape>;
49+
} = {},
50+
) => {
51+
type Dto = $.computeObjectShape<DBType['__pointers__'], HydratedShape>;
52+
53+
const resource = EnhancedResource.of(resourceIn);
54+
55+
const hydrate = e.shape(
56+
resource.db,
57+
(options.hydrate ?? ((obj: any) => obj['*'])) as any,
58+
) as (scope: unknown) => HydratedShape;
59+
60+
@Injectable()
61+
abstract class Repository extends CommonRepository {
62+
static customize<Customized extends Repository>(
63+
customizer: (cls: typeof Repository) => AbstractClass<Customized>,
64+
): AbstractClass<
65+
Customized & Omit<DefaultDtoRepository, keyof Customized>
66+
> {
67+
const customizedClass = customizer(Repository);
68+
const customMethodNames = setOf(
69+
Object.getOwnPropertyNames(customizedClass.prototype),
70+
);
71+
const nonDeclaredDefaults = mapKeys(
72+
Object.getOwnPropertyDescriptors(DefaultDtoRepository.prototype),
73+
(name, _, { SKIP }) =>
74+
typeof name === 'string' && customMethodNames.has(name) ? SKIP : name,
75+
).asRecord;
76+
Object.defineProperties(customizedClass.prototype, nonDeclaredDefaults);
77+
78+
return customizedClass as any;
79+
}
80+
static withDefaults() {
81+
return DefaultDtoRepository;
82+
}
83+
84+
@Inject(Privileges)
85+
protected readonly [privileges]: Privileges;
86+
protected readonly resource = resource;
87+
protected readonly hydrate = hydrate;
88+
89+
@Once()
90+
get privileges() {
91+
return this[privileges].forResource(resource);
92+
}
93+
94+
getActualChanges = getChanges(resource.type);
95+
96+
// region List Helpers
97+
98+
protected listFilters(
99+
_scope: ScopeOf<$.TypeSet<DBType>>,
100+
_input: any,
101+
): Many<SelectFilterExpression | false | Nil> {
102+
return [];
103+
}
104+
105+
protected orderBy<Scope extends $expr_PathNode>(
106+
scope: ScopeOf<$.TypeSet<DBType>>,
107+
input: SortablePaginationInput,
108+
) {
109+
// TODO Validate this is a valid sort key
110+
const sortKey = input.sort as keyof Scope['*'];
111+
return {
112+
expression: scope[sortKey],
113+
direction: input.order,
114+
} as const;
115+
}
116+
117+
protected async paginate(
118+
listOfAllQuery: $expr_Select<
119+
$.TypeSet<$.ObjectType<DBType['__name__']>, $.Cardinality.Many>
120+
>,
121+
input: PaginationInput,
122+
) {
123+
const thisPage = e.select(listOfAllQuery as any, () => ({
124+
offset: (input.page - 1) * input.count,
125+
limit: input.count + 1,
126+
}));
127+
const items = e.select(thisPage, (obj) => ({
128+
...this.hydrate(obj),
129+
limit: input.count,
130+
}));
131+
const query = e.select({
132+
items,
133+
total: e.count(listOfAllQuery),
134+
hasMore: e.op(e.count(thisPage), '>', input.count),
135+
});
136+
137+
const result = await this.db.run(query);
138+
return result as PaginatedListType<Dto>;
139+
}
140+
141+
// endregion
142+
143+
/**
144+
* Here for compatibility with the Neo4j version.
145+
* @deprecated this should be replaced with just error handling from a
146+
* failed insert, after we finish migration.
147+
*/
148+
async isUnique(value: string, fqn?: string) {
149+
const res = fqn ? await this.resources.getByEdgeDB(fqn) : resource;
150+
const query = e.select(e.Mixin.Named, (obj) => ({
151+
filter: e.op(
152+
e.op(obj.name, '=', value),
153+
'and',
154+
e.op(obj.__type__.name, '=', res.dbFQN as string),
155+
),
156+
limit: 1,
157+
}));
158+
159+
const found = await this.db.run(query);
160+
return found.length === 0;
161+
}
162+
163+
async getBaseNodes(ids: readonly ID[], fqn?: ResourceLike) {
164+
return await super.getBaseNodes(ids, fqn ?? resource);
165+
}
166+
}
167+
168+
class DefaultDtoRepository extends Repository {
169+
async readOne(id: ID) {
170+
const rows = await this.readMany([id]);
171+
if (!rows[0]) {
172+
throw new NotFoundException(
173+
`Could not find ${lowerCase(this.resource.name)}`,
174+
);
175+
}
176+
return rows[0];
177+
}
178+
179+
async readMany(ids: readonly ID[]): Promise<readonly Dto[]> {
180+
const query = e.params({ ids: e.array(e.uuid) }, ({ ids }) =>
181+
e.select(this.resource.db, (obj: any) => ({
182+
...(this.hydrate(obj) as any),
183+
filter: e.op(obj.id, 'in', e.array_unpack(ids)),
184+
})),
185+
);
186+
const rows = await this.db.run(query, { ids });
187+
return rows as readonly Dto[];
188+
}
189+
190+
async list(input: PaginationInput) {
191+
const all = e.select(this.resource.db, (obj: any) => {
192+
const filters = many(this.listFilters(obj, input)).filter(isNotFalsy);
193+
const filter =
194+
filters.length === 0
195+
? null
196+
: filters.length === 1
197+
? filters[0]
198+
: e.all(e.set(...filters));
199+
return {
200+
...(filter ? { filter } : {}),
201+
...(input instanceof SortablePaginationInput
202+
? { order_by: this.orderBy(obj, input as SortablePaginationInput) }
203+
: {}),
204+
};
205+
});
206+
return await this.paginate(all as any, input);
207+
}
208+
209+
async create(input: Omit<InsertShape<DBType>, `@${string}`>): Promise<Dto> {
210+
const query = e.select(
211+
e.insert(this.resource.db, input as any),
212+
this.hydrate as any,
213+
);
214+
return (await this.db.run(query)) as Dto;
215+
}
216+
217+
async update(
218+
existing: Pick<Dto, 'id'>,
219+
input: UpdateShape<TResourceStatic['DB'] & {}>,
220+
): Promise<Dto> {
221+
const query = e.select(
222+
e.update(this.resource.db, () => ({
223+
filter_single: { id: existing.id } as any,
224+
set: input,
225+
})),
226+
this.hydrate as any,
227+
);
228+
return (await this.db.run(query)) as Dto;
229+
}
230+
231+
async delete(id: ID): Promise<void> {
232+
const query = e.delete(this.resource.db, () => ({
233+
filter_single: { id } as any,
234+
}));
235+
await this.db.run(query);
236+
}
237+
}
238+
239+
return Repository;
240+
};
241+
242+
type ShapeFn<
243+
Expr extends $.ObjectTypeExpression,
244+
Shape extends objectTypeToSelectShape<Expr['__element__']> &
245+
SelectModifiers<Expr['__element__']>,
246+
> = (scope: ScopeOf<Expr>) => Readonly<Shape>;
247+
248+
export type ScopeOf<Expr extends $.ObjectTypeExpression> = $.$scopify<
249+
Expr['__element__']
250+
> &
251+
$linkPropify<{
252+
[k in keyof Expr]: k extends '__cardinality__'
253+
? $.Cardinality.One
254+
: Expr[k];
255+
}>;

src/core/edgedb/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './edgedb.service';
44
export * from './withScope';
55
export * from './exclusivity-violation.error';
66
export * from './common.repository';
7+
export * from './dto.repository';

0 commit comments

Comments
 (0)