Skip to content

Commit 53085a0

Browse files
authored
Merge pull request #2886 from SeedCompany/file-hydrate-root-attachment
2 parents f464fc3 + 9033f41 commit 53085a0

File tree

6 files changed

+107
-39
lines changed

6 files changed

+107
-39
lines changed

src/components/file/dto/node.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
44
import { Readable } from 'stream';
55
import { keys as keysOf } from 'ts-transformer-keys';
66
import { MergeExclusive, Opaque } from 'type-fest';
7+
import { BaseNode } from '~/core/database/results';
78
import { RegisterResource } from '~/core/resources';
89
import {
910
DateTimeField,
@@ -69,6 +70,12 @@ abstract class FileNode extends Resource {
6970
readonly public: boolean;
7071

7172
readonly createdById: ID;
73+
74+
/** The root FileNode. This could be self */
75+
readonly root: BaseNode;
76+
77+
/** The resource the root FileNode is attached to */
78+
readonly rootAttachedTo: [resource: BaseNode, relationName: string];
7279
}
7380

7481
// class name has to match schema name for interface resolvers to work.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { File } from '../dto';
2+
3+
/**
4+
* Emitted as the last step of the file upload process.
5+
* Feel free to throw to abort mutation.
6+
*/
7+
export class AfterFileUploadEvent {
8+
constructor(readonly file: File) {}
9+
}

src/components/file/file.repository.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from 'cypher-query-builder';
1212
import { Direction } from 'cypher-query-builder/dist/typings/clauses/order-by';
1313
import { AnyConditions } from 'cypher-query-builder/dist/typings/clauses/where-utils';
14-
import { isEmpty } from 'lodash';
14+
import { isEmpty, keyBy } from 'lodash';
1515
import { DateTime } from 'luxon';
1616
import {
1717
ID,
@@ -26,6 +26,7 @@ import {
2626
ILogger,
2727
Logger,
2828
OnIndex,
29+
ResourceRef,
2930
} from '../../core';
3031
import {
3132
ACTIVE,
@@ -174,7 +175,27 @@ export class FileRepository extends CommonRepository {
174175
.where({ node: hasLabel(FileNodeType.Directory) })
175176
.apply(this.hydrateDirectory()),
176177
)
177-
.return<{ dto: FileNode }>('dto');
178+
.subQuery('node', (sub) =>
179+
sub
180+
.raw('MATCH p=(node)-[:parent*0..]->(root:FileNode)')
181+
.return('root')
182+
.orderBy('length(p)', 'DESC')
183+
.raw('LIMIT 1'),
184+
)
185+
.subQuery('root', (sub) =>
186+
sub
187+
.raw('MATCH (resource:BaseNode)-[rel]->(root)')
188+
// Need to filter out FileNodes which are children of this dir
189+
// (the schema was mistakenly pointing these relationships in the wrong direction)
190+
// Also filter to ACTIVE, if applicable.
191+
.raw(
192+
'WHERE NOT resource:FileNode AND coalesce(rel.active, true) <> false',
193+
)
194+
.return('[resource, type(rel)] as rootAttachedTo'),
195+
)
196+
.return<{ dto: FileNode }>(
197+
merge('dto', keyBy(['root', 'rootAttachedTo'])).as('dto'),
198+
);
178199
}
179200

180201
private hydrateFile() {
@@ -386,6 +407,42 @@ export class FileRepository extends CommonRepository {
386407
return result.id;
387408
}
388409

410+
async createRootDirectory({
411+
resource,
412+
relation,
413+
name,
414+
public: isPublic,
415+
session,
416+
}: {
417+
resource: ResourceRef<any>;
418+
relation: string;
419+
name: string;
420+
public?: boolean;
421+
session: Session;
422+
}) {
423+
const initialProps = {
424+
name,
425+
public: isPublic,
426+
};
427+
428+
const query = this.db
429+
.query()
430+
.apply(await createNode(Directory, { initialProps }))
431+
.apply(
432+
createRelationships(Directory, {
433+
in: { [relation]: ['BaseNode', resource.id] },
434+
out: { createdBy: ['User', session.userId] },
435+
}),
436+
)
437+
.return<{ id: ID }>('node.id as id');
438+
439+
const result = await query.first();
440+
if (!result) {
441+
throw new ServerException('Failed to create directory');
442+
}
443+
return result.id;
444+
}
445+
389446
async createFile({
390447
fileId,
391448
name,

src/components/file/file.service.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Connection } from 'cypher-query-builder';
88
import { intersection } from 'lodash';
99
import { Duration } from 'luxon';
1010
import { Readable } from 'stream';
11-
import { withAddedPath } from '~/common/url.util';
1211
import {
1312
DuplicateException,
1413
DurationIn,
@@ -19,8 +18,9 @@ import {
1918
ServerException,
2019
Session,
2120
UnauthorizedException,
22-
} from '../../common';
23-
import { ConfigService, ILogger, Logger } from '../../core';
21+
} from '~/common';
22+
import { withAddedPath } from '~/common/url.util';
23+
import { ConfigService, IEventBus, ILogger, Logger } from '~/core';
2424
import { FileBucket } from './bucket';
2525
import {
2626
CreateDefinedFileVersionInput,
@@ -41,6 +41,7 @@ import {
4141
RenameFileInput,
4242
RequestUploadOutput,
4343
} from './dto';
44+
import { AfterFileUploadEvent } from './events/after-file-upload.event';
4445
import { FileUrlController as FileUrl } from './file-url.controller';
4546
import { FileRepository } from './file.repository';
4647
import { MediaService } from './media/media.service';
@@ -53,6 +54,7 @@ export class FileService {
5354
private readonly db: Connection,
5455
private readonly config: ConfigService,
5556
private readonly mediaService: MediaService,
57+
private readonly eventBus: IEventBus,
5658
@Logger('file:service') private readonly logger: ILogger,
5759
) {}
5860

@@ -225,6 +227,12 @@ export class FileService {
225227
return await this.getDirectory(id, session);
226228
}
227229

230+
async createRootDirectory(
231+
...args: Parameters<FileRepository['createRootDirectory']>
232+
) {
233+
return await this.repo.createRootDirectory(...args);
234+
}
235+
228236
async requestUpload(): Promise<RequestUploadOutput> {
229237
const id = await generateId();
230238
const url = await this.bucket.getSignedUrl(PutObject, {
@@ -361,7 +369,11 @@ export class FileService {
361369
// Change the file's name to match the latest version name
362370
await this.rename({ id: fileId, name }, session);
363371

364-
return await this.getFile(fileId, session);
372+
const file = await this.getFile(fileId, session);
373+
374+
await this.eventBus.publish(new AfterFileUploadEvent(file));
375+
376+
return file;
365377
}
366378

367379
private async validateParentNode(
Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { node, relation } from 'cypher-query-builder';
2-
import { DateTime } from 'luxon';
3-
import { DatabaseService, EventsHandler, IEventHandler } from '../../../core';
1+
import { DatabaseService, EventsHandler, IEventHandler } from '~/core';
42
import { ProjectCreatedEvent } from '../../project/events';
53
import { FileService } from '../file.service';
64

@@ -15,32 +13,17 @@ export class AttachProjectRootDirectoryHandler
1513

1614
async handle(event: ProjectCreatedEvent) {
1715
const { project, session } = event;
18-
const { id } = project;
1916

20-
const rootDir = await this.files.createDirectory(
21-
undefined,
22-
`${id} root directory`,
17+
const rootDirId = await this.files.createRootDirectory({
18+
resource: project,
19+
relation: 'rootDirectory',
20+
name: `${project.id} root directory`,
2321
session,
24-
);
22+
});
2523

26-
await this.db
27-
.query()
28-
.match([
29-
[node('project', 'Project', { id })],
30-
[node('dir', 'Directory', { id: rootDir.id })],
31-
])
32-
.create([
33-
node('project'),
34-
relation('out', '', 'rootDirectory', {
35-
active: true,
36-
createdAt: DateTime.local(),
37-
}),
38-
node('dir'),
39-
])
40-
.run();
4124
event.project = {
4225
...event.project,
43-
rootDirectory: rootDir.id,
26+
rootDirectory: rootDirId,
4427
};
4528

4629
const folders = [
@@ -50,7 +33,7 @@ export class AttachProjectRootDirectoryHandler
5033
'Photos',
5134
];
5235
for (const folder of folders) {
53-
await this.files.createDirectory(rootDir.id, folder, session);
36+
await this.files.createDirectory(rootDirId, folder, session);
5437
}
5538
}
5639
}

test/utility/create-directory.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ export async function createRootDirectory(app: TestApp, name?: string) {
1414
.get(AuthenticationService)
1515
.resumeSession(app.graphql.authToken);
1616
const session = loggedInSession(rawSession);
17-
const actual = await app
18-
.get(FileService)
19-
.createDirectory(undefined, name, session);
20-
21-
expect(actual).toBeTruthy();
22-
expect(actual.name).toBe(name);
23-
24-
return actual;
17+
const id = await app.get(FileService).createRootDirectory({
18+
// An attachment point is required, so just use the current user.
19+
resource: { __typename: 'User', id: session.userId },
20+
relation: 'dir',
21+
name,
22+
session,
23+
});
24+
return await app.get(FileService).getDirectory(id, session);
2525
}
2626

2727
export async function createDirectory(

0 commit comments

Comments
 (0)