Skip to content

Commit db6cdfa

Browse files
authored
Merge pull request #3172 from SeedCompany/bugfix/normalize-creator
2 parents 2accaa0 + 0d84113 commit db6cdfa

File tree

12 files changed

+101
-32
lines changed

12 files changed

+101
-32
lines changed

src/common/secured-property.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export type SecuredKeys<Dto extends Record<string, any>> = ConditionalKeys<
3333

3434
export type MaybeSecured<Dto> = Dto | UnsecuredDto<Dto>;
3535

36+
export type MaybeSecuredProp<T> = T | Secured<T>;
37+
3638
/**
3739
* Converts a DTO to unwrap its secured properties.
3840
* Non-secured properties are left as is.

src/components/admin/admin.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AdminEdgeDBRepository } from './admin.edgedb.repository';
55
import { AdminEdgeDBService } from './admin.edgedb.service';
66
import { AdminRepository } from './admin.repository';
77
import { AdminService } from './admin.service';
8+
import { NormalizeCreatorMigration } from './migrations/normalize-creator.migration';
89

910
@Module({
1011
imports: [AuthorizationModule],
@@ -16,6 +17,7 @@ import { AdminService } from './admin.service';
1617
// and each will only use their own.
1718
AdminEdgeDBRepository,
1819
),
20+
NormalizeCreatorMigration,
1921
],
2022
})
2123
export class AdminModule {}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { BaseMigration, Migration } from '~/core/database';
2+
3+
@Migration('2024-04-16T19:00:00')
4+
export class NormalizeCreatorMigration extends BaseMigration {
5+
async up() {
6+
// Handle RootUser first specially.
7+
// Since denormalized creator IDs for RootUser currently reference a non-existent user ID
8+
await this.db.query().raw`
9+
match (user:RootUser)
10+
match (bn)-[r:creator { active: true }]->(oldCreatorProp:Property)
11+
where not exists { (:User { id: oldCreatorProp.value }) }
12+
create (bn)-[:creator { active: true, createdAt: r.createdAt }]->(user)
13+
detach delete oldCreatorProp
14+
`.executeAndLogStats();
15+
16+
await this.db.query().raw`
17+
match (bn)-[r:creator { active: true }]->(oldCreatorProp:Property)
18+
with bn, r, oldCreatorProp
19+
match (user:User { id: oldCreatorProp.value })
20+
create (bn)-[:creator { active: true, createdAt: r.createdAt }]->(user)
21+
detach delete oldCreatorProp
22+
`.executeAndLogStats();
23+
}
24+
}

src/components/authorization/policies/conditions/owner.condition.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { Logger } from '@nestjs/common';
22
import { Query } from 'cypher-query-builder';
33
import { inspect, InspectOptionsStylized } from 'util';
4-
import { ID, isIdLike, MaybeSecured, ResourceShape, Secured } from '~/common';
4+
import {
5+
ID,
6+
isIdLike,
7+
MaybeSecured,
8+
MaybeSecuredProp,
9+
ResourceShape,
10+
unwrapSecured,
11+
} from '~/common';
12+
import { type LinkTo } from '~/core/resources';
513
import { User } from '../../../user/dto';
614
import {
715
AsCypherParams,
@@ -12,7 +20,7 @@ import {
1220
const CQL_VAR = 'requestingUser';
1321

1422
export interface HasCreator {
15-
creator: ID | Secured<ID>;
23+
creator: MaybeSecuredProp<ID | LinkTo<'User'>>;
1624
}
1725

1826
class OwnerCondition<
@@ -29,11 +37,11 @@ class OwnerCondition<
2937
return (object as MaybeSecured<User>).id;
3038
}
3139
const o = object as MaybeSecured<HasCreator>;
32-
return !o.creator
33-
? undefined
34-
: isIdLike(o.creator)
35-
? o.creator
36-
: o.creator.value;
40+
const creator = unwrapSecured(o.creator);
41+
if (!creator) {
42+
return undefined;
43+
}
44+
return isIdLike(creator) ? creator : creator.id;
3745
})();
3846
if (!creator) {
3947
Logger.warn(

src/components/post/dto/post.dto.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { DateTime } from 'luxon';
33
import { keys as keysOf } from 'ts-transformer-keys';
44
import { BaseNode } from '~/core/database/results';
55
import { e } from '~/core/edgedb';
6-
import { RegisterResource } from '~/core/resources';
6+
import { LinkTo, RegisterResource } from '~/core/resources';
77
import {
88
DateTimeField,
9-
ID,
109
Resource,
1110
Secured,
1211
SecuredProps,
@@ -26,7 +25,7 @@ export class Post extends Resource {
2625

2726
readonly parent: BaseNode;
2827

29-
readonly creator: Secured<ID>;
28+
readonly creator: Secured<LinkTo<'User'>>;
3029

3130
@Field(() => PostType)
3231
readonly type: PostType;

src/components/post/post.repository.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export class PostRepository extends DtoRepository<typeof Post, [Session] | []>(
2424
) {
2525
async create(input: CreatePost, session: Session) {
2626
const initialProps = {
27-
creator: session.userId,
2827
type: input.type,
2928
shareability: input.shareability,
3029
body: input.body,
@@ -35,8 +34,13 @@ export class PostRepository extends DtoRepository<typeof Post, [Session] | []>(
3534
.apply(matchRequestingUser(session))
3635
.apply(await createNode(Post, { initialProps }))
3736
.apply(
38-
createRelationships(Post, 'in', {
39-
post: ['BaseNode', input.parentId],
37+
createRelationships(Post, {
38+
in: {
39+
post: ['BaseNode', input.parentId],
40+
},
41+
out: {
42+
creator: ['User', session.userId],
43+
},
4044
}),
4145
)
4246
.apply(this.hydrate())
@@ -115,10 +119,16 @@ export class PostRepository extends DtoRepository<typeof Post, [Session] | []>(
115119
relation('in', '', 'post', ACTIVE),
116120
node('parent', 'BaseNode'),
117121
])
122+
.match([
123+
node('node'),
124+
relation('out', '', 'creator', ACTIVE),
125+
node('creator', 'User'),
126+
])
118127
.apply(matchProps())
119128
.return<{ dto: DbTypeOf<Post> }>(
120129
merge('props', {
121130
parent: 'parent',
131+
creator: 'creator { .id }',
122132
}).as('dto'),
123133
);
124134
}

src/components/post/post.resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class PostResolver {
5555
@Parent() post: Post,
5656
@Loader(UserLoader) users: LoaderOf<UserLoader>,
5757
): Promise<SecuredUser> {
58-
return await mapSecuredValue(post.creator, (id) => users.load(id));
58+
return await mapSecuredValue(post.creator, ({ id }) => users.load(id));
5959
}
6060

6161
@Mutation(() => UpdatePostOutput, {

src/components/progress-report/media/dto/media.dto.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import {
99
Variant,
1010
VariantOf,
1111
} from '~/common';
12-
import { SetDbType } from '~/core';
12+
import { LinkTo, SetDbType } from '~/core';
1313
import { e } from '~/core/edgedb';
1414
import { RegisterResource } from '~/core/resources';
1515
import { FileId, Media } from '../../../file';
16-
import { User } from '../../../user';
1716
import { ProgressReport } from '../../dto';
1817
import { ProgressReportHighlight } from '../../dto/highlights.dto';
1918
import { MediaCategory } from '../media-category.enum';
@@ -53,7 +52,7 @@ export class ProgressReportMedia extends Resource {
5352
@IdField()
5453
readonly variantGroup: VariantGroup;
5554

56-
readonly creator: IdOf<User>;
55+
readonly creator: LinkTo<'User'>;
5756
}
5857

5958
export type MediaVariant = VariantOf<typeof ProgressReportMedia>;

src/components/progress-report/media/progress-report-media.repository.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
import { DbTypeOf, DtoRepository } from '~/core';
1313
import {
1414
ACTIVE,
15-
apoc,
1615
createNode,
1716
createRelationships,
1817
filter,
@@ -169,10 +168,14 @@ export class ProgressReportMediaRepository extends DtoRepository<
169168
baseNodeProps: {
170169
variant: input.variant.key,
171170
category: input.category,
172-
creator: session.userId,
173171
},
174172
}),
175173
)
174+
.apply(
175+
createRelationships(this.resource, 'out', {
176+
creator: ['User', session.userId],
177+
}),
178+
)
176179
.apply(
177180
createRelationships(this.resource, 'in', {
178181
child: variable('variantGroup'),
@@ -184,7 +187,10 @@ export class ProgressReportMediaRepository extends DtoRepository<
184187
}),
185188
)
186189
.return<{ dto: Omit<DbTypeOf<ReportMedia>, 'media' | 'file'> }>(
187-
apoc.convert.toMap('node').as('dto'),
190+
merge('node', {
191+
report: 'report.id',
192+
creator: 'creator { .id }',
193+
}).as('dto'),
188194
);
189195
const results = await query.first();
190196
if (results) {
@@ -262,6 +268,11 @@ export class ProgressReportMediaRepository extends DtoRepository<
262268
relation('out', '', 'fileNode', ACTIVE),
263269
node('file', 'File'),
264270
],
271+
[
272+
node('node'),
273+
relation('out', '', 'creator', ACTIVE),
274+
node('creator', 'User'),
275+
],
265276
])
266277
.subQuery('file', (sub) =>
267278
sub
@@ -282,6 +293,7 @@ export class ProgressReportMediaRepository extends DtoRepository<
282293
variantGroup: 'variantGroup.id',
283294
file: 'file.id',
284295
media: 'media.id',
296+
creator: 'creator { .id }',
285297
sensitivity: 'sensitivity',
286298
scope: 'scope',
287299
}).as('dto'),

src/components/prompts/dto/prompt-response.dto.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ import {
1818
UnsecuredDto,
1919
Variant,
2020
} from '~/common';
21-
import { LinkToUnknown } from '~/core';
21+
import { LinkTo, LinkToUnknown } from '~/core';
2222
import { BaseNode } from '~/core/database/results';
23-
import { User } from '../../user';
2423
import { Prompt, SecuredPrompt } from './prompt.dto';
2524

2625
@ObjectType()
@@ -46,7 +45,7 @@ export abstract class VariantResponse<Key extends string = string> {
4645
@Field()
4746
readonly response: SecuredRichTextNullable;
4847

49-
readonly creator: Secured<IdOf<User>>;
48+
readonly creator: Secured<LinkTo<'User'>>;
5049

5150
@Field(() => DateTime, { nullable: true })
5251
readonly modifiedAt?: DateTime;
@@ -65,7 +64,7 @@ export class PromptVariantResponse<
6564
responses: [VariantResponse],
6665
} satisfies ResourceRelationsShape;
6766

68-
readonly creator: Secured<IdOf<User>>;
67+
readonly creator: Secured<LinkTo<'User'>>;
6968

7069
readonly parent: BaseNode;
7170

0 commit comments

Comments
 (0)