Skip to content

Commit 2579276

Browse files
authored
Merge pull request #3071 from SeedCompany/edgedb/alias
2 parents f399a95 + 9b2acf9 commit 2579276

File tree

9 files changed

+155
-29
lines changed

9 files changed

+155
-29
lines changed

dbschema/alias.esdl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module default {
2+
type Alias {
3+
required name: str {
4+
constraint exclusive;
5+
};
6+
required target: Object {
7+
on target delete delete source;
8+
};
9+
}
10+
}

dbschema/migrations/00051.edgeql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE MIGRATION m1tgd6yl63z2bp7ahtbi7sidb5gfl7mjs47ooqfbnuleffpap3auua
2+
ONTO m1vlgekf4eyzweb6xmldmxwzohoh7xbrre6fbc7e2d3t7q7ry3qobq
3+
{
4+
CREATE TYPE default::Alias {
5+
CREATE REQUIRED LINK target: std::Object {
6+
ON TARGET DELETE DELETE SOURCE;
7+
};
8+
CREATE REQUIRED PROPERTY name: std::str {
9+
CREATE CONSTRAINT std::exclusive;
10+
};
11+
};
12+
};

src/common/id.arg.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PipeTransform, Type } from '@nestjs/common';
22
import { Args, ArgsOptions, ID as IdType } from '@nestjs/graphql';
3-
import { ValidateIdPipe } from './validators';
3+
import { ValidateIdPipe } from './validators/short-id.validator';
44

55
export const IdArg = (
66
opts: Partial<ArgsOptions> = {},

src/common/validators/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export * from './email.validator';
22
export * from './iana-timezone.validator';
33
export * from './iso-3166-1-alpha-3.validator';
4-
export * from './short-id.validator';
4+
export { IsId } from './short-id.validator';

src/common/validators/short-id.validator.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,54 @@
1-
import { ArgumentMetadata, PipeTransform } from '@nestjs/common';
2-
import { ValidationOptions } from 'class-validator';
1+
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
2+
import {
3+
ValidationArguments,
4+
ValidationOptions,
5+
ValidatorConstraint,
6+
ValidatorConstraintInterface,
7+
} from 'class-validator';
38
import { ValidationException } from '~/core/validation';
49
import { isValidId } from '../generate-id';
10+
import { ID } from '../id-field';
511
import { ValidateBy } from './validateBy';
612

713
export const IsId = (validationOptions?: ValidationOptions) =>
8-
ValidateBy(
9-
{
10-
name: 'IsId',
11-
validator: {
12-
validate: isValidId,
13-
defaultMessage: () =>
14-
validationOptions?.each
15-
? 'Each value in $property must be a valid ID'
16-
: 'Invalid ID',
17-
},
18-
},
19-
validationOptions,
20-
);
14+
ValidateBy(ValidIdConstraint, {
15+
message: validationOptions?.each
16+
? 'Each value in $property must be a valid ID'
17+
: 'Invalid ID',
18+
...validationOptions,
19+
});
20+
21+
@Injectable()
22+
export class IdResolver {
23+
async resolve(value: ID): Promise<ID> {
24+
return value;
25+
}
26+
}
27+
28+
@Injectable()
29+
@ValidatorConstraint({ name: 'IsId', async: true })
30+
export class ValidIdConstraint implements ValidatorConstraintInterface {
31+
constructor(private readonly resolver: IdResolver) {}
2132

33+
async validate(value: unknown, args: ValidationArguments) {
34+
if (isValidId(value)) {
35+
(args.object as any)[args.property] = await this.resolver.resolve(value);
36+
return true;
37+
}
38+
return false;
39+
}
40+
}
41+
42+
@Injectable()
2243
export class ValidateIdPipe implements PipeTransform {
44+
constructor(private readonly resolver: IdResolver) {}
45+
2346
transform(id: unknown, metadata: ArgumentMetadata) {
24-
if (id == null || isValidId(id)) {
25-
return id;
47+
if (id == null) {
48+
return null;
49+
}
50+
if (isValidId(id)) {
51+
return this.resolver.resolve(id);
2652
}
2753
throw new ValidationException([
2854
{

src/components/changeset/changeset.arg.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,10 @@ import {
77
import { Args, ArgsOptions, ID as IDType } from '@nestjs/graphql';
88
import { Resolver } from '@nestjs/graphql/dist/enums/resolver.enum.js';
99
import { RESOLVER_TYPE_METADATA as TypeKey } from '@nestjs/graphql/dist/graphql.constants.js';
10-
import {
11-
ID,
12-
InputException,
13-
ServerException,
14-
ValidateIdPipe,
15-
} from '../../common';
16-
import { createAugmentedMetadataPipe } from '../../common/augmented-metadata.pipe';
17-
import { ResourceLoader } from '../../core';
10+
import { ID, InputException, ServerException } from '~/common';
11+
import { createAugmentedMetadataPipe } from '~/common/augmented-metadata.pipe';
12+
import { ValidateIdPipe } from '~/common/validators/short-id.validator';
13+
import { ResourceLoader } from '~/core/resources';
1814

1915
const pipeMetadata = createAugmentedMetadataPipe<{
2016
mutation: boolean;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { isUUID } from 'class-validator';
3+
import DataLoader from 'dataloader';
4+
import LRU from 'lru-cache';
5+
import { ID, NotFoundException } from '~/common';
6+
import { IdResolver } from '~/common/validators/short-id.validator';
7+
import { ILogger, Logger } from '~/core/logger';
8+
import { EdgeDB } from './edgedb.service';
9+
import { e } from './reexports';
10+
11+
@Injectable()
12+
export class AliasIdResolver implements IdResolver {
13+
private readonly loader: DataLoader<ID, ID>;
14+
15+
constructor(
16+
private readonly db: EdgeDB,
17+
@Logger('alias-resolver') private readonly logger: ILogger,
18+
) {
19+
this.loader = new DataLoader((x) => this.loadMany(x), {
20+
// Since this loader exists for the lifetime of the process
21+
// and there's no cache invalidation, we'll just use an LRU cache
22+
cacheMap: new LRU({
23+
max: 10_000,
24+
}),
25+
});
26+
}
27+
28+
async resolve(value: ID): Promise<ID> {
29+
try {
30+
return await this.loader.load(value);
31+
} catch (e) {
32+
if (e instanceof NotFoundException) {
33+
this.loader.clear(value); // maybe it'll be there next request
34+
return value; // assume valid or defer error
35+
}
36+
throw e;
37+
}
38+
}
39+
40+
async loadMany(ids: readonly ID[]): Promise<ReadonlyArray<ID | Error>> {
41+
const aliases = ids.filter((id) => {
42+
if (isUUID(id)) {
43+
return false;
44+
}
45+
return true;
46+
});
47+
if (aliases.length === 0) {
48+
return ids;
49+
}
50+
51+
this.logger.info('Resolving aliases', { ids: aliases });
52+
const foundList = await this.db.run(this.query, { aliasList: aliases });
53+
return ids.map((id) => {
54+
const found = foundList.find((f) => f.name === id);
55+
return found
56+
? found.targetId
57+
: !aliases.includes(id)
58+
? id
59+
: new NotFoundException();
60+
});
61+
}
62+
63+
private readonly query = e.params(
64+
{ aliasList: e.array(e.str) },
65+
({ aliasList }) =>
66+
e.select(e.Alias, (alias) => ({
67+
filter: e.op(alias.name, 'in', e.array_unpack(aliasList)),
68+
name: true,
69+
targetId: alias.target.id,
70+
})),
71+
);
72+
}

src/core/edgedb/edgedb.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Module, OnModuleDestroy } from '@nestjs/common';
22
import { APP_INTERCEPTOR } from '@nestjs/core';
33
import { createClient, Duration } from 'edgedb';
4+
import { IdResolver } from '~/common/validators/short-id.validator';
45
import type { ConfigService } from '~/core';
6+
import { splitDb } from '../database/split-db.provider';
7+
import { AliasIdResolver } from './alias-id-resolver';
58
import { codecs, registerCustomScalarCodecs } from './codecs';
69
import { EdgeDBTransactionalMutationsInterceptor } from './edgedb-transactional-mutations.interceptor';
710
import { EdgeDB } from './edgedb.service';
@@ -49,8 +52,9 @@ import { TransactionContext } from './transaction.context';
4952
provide: APP_INTERCEPTOR,
5053
useClass: EdgeDBTransactionalMutationsInterceptor,
5154
},
55+
splitDb(IdResolver, AliasIdResolver),
5256
],
53-
exports: [EdgeDB, Client],
57+
exports: [EdgeDB, Client, IdResolver],
5458
})
5559
export class EdgeDBModule implements OnModuleDestroy {
5660
constructor(private readonly client: Client) {}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { Module } from '@nestjs/common';
22
import { APP_PIPE } from '@nestjs/core';
33
import { Validator } from 'class-validator';
4+
import {
5+
ValidateIdPipe,
6+
ValidIdConstraint,
7+
} from '~/common/validators/short-id.validator';
48
import { ValidationPipe } from './validation.pipe';
59

610
@Module({
711
providers: [
812
Validator,
913
ValidationPipe,
1014
{ provide: APP_PIPE, useExisting: ValidationPipe },
15+
ValidIdConstraint,
16+
ValidateIdPipe,
1117
],
12-
exports: [ValidationPipe],
18+
exports: [ValidationPipe, ValidateIdPipe],
1319
})
1420
export class ValidationModule {}

0 commit comments

Comments
 (0)