Skip to content

Commit efa9e96

Browse files
authored
Merge pull request #132 from import-ai/feat/shares
feat(shares): add shares API
2 parents 941f56e + d493052 commit efa9e96

File tree

11 files changed

+340
-1
lines changed

11 files changed

+340
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"build": "nest build",
7-
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
7+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
88
"start": "nest start",
99
"start:dev": "nest start --watch",
1010
"start:debug": "nest start --debug --watch",

src/app/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { Init1751900000000 } from 'omniboxd/migrations/1751900000000-init';
3434
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
3535
import { NullUserEmail1752814358259 } from 'omniboxd/migrations/1752814358259-null-user-email';
3636
import { AttachmentsModule } from 'omniboxd/attachments/attachments.module';
37+
import { Shares1753866547335 } from 'omniboxd/migrations/1753866547335-shares';
38+
import { SharesModule } from 'omniboxd/shares/shares.module';
3739

3840
@Module({})
3941
export class AppModule implements NestModule {
@@ -72,6 +74,7 @@ export class AppModule implements NestModule {
7274
SearchModule,
7375
InvitationsModule,
7476
AttachmentsModule,
77+
SharesModule,
7578
// CacheModule.registerAsync({
7679
// imports: [ConfigModule],
7780
// inject: [ConfigService],
@@ -98,6 +101,7 @@ export class AppModule implements NestModule {
98101
UserOptions1751904560034,
99102
UserBindings1752652489640,
100103
NullUserEmail1752814358259,
104+
Shares1753866547335,
101105
...extraMigrations,
102106
],
103107
migrationsRun: true,
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
2+
import { BaseColumns } from './base-columns';
3+
4+
async function createShareTypeEnum(queryRunner: QueryRunner): Promise<void> {
5+
await queryRunner.query(`
6+
CREATE TYPE share_type AS ENUM (
7+
'doc_only',
8+
'chat_only',
9+
'all'
10+
);
11+
`);
12+
}
13+
14+
async function createSharesTable(queryRunner: QueryRunner): Promise<void> {
15+
const table = new Table({
16+
name: 'shares',
17+
columns: [
18+
{
19+
name: 'id',
20+
type: 'character varying',
21+
isPrimary: true,
22+
},
23+
{
24+
name: 'namespace_id',
25+
type: 'character varying',
26+
isNullable: false,
27+
},
28+
{
29+
name: 'resource_id',
30+
type: 'character varying',
31+
isNullable: false,
32+
},
33+
{
34+
name: 'enabled',
35+
type: 'boolean',
36+
isNullable: false,
37+
},
38+
{
39+
name: 'require_login',
40+
type: 'boolean',
41+
isNullable: false,
42+
},
43+
{
44+
name: 'share_type',
45+
type: 'share_type',
46+
isNullable: false,
47+
},
48+
{
49+
name: 'password',
50+
type: 'character varying',
51+
isNullable: true,
52+
},
53+
{
54+
name: 'expires_at',
55+
type: 'timestamp with time zone',
56+
isNullable: true,
57+
},
58+
...BaseColumns(),
59+
],
60+
indices: [
61+
{
62+
columnNames: ['namespace_id', 'resource_id'],
63+
isUnique: true,
64+
where: 'deleted_at IS NULL',
65+
},
66+
],
67+
foreignKeys: [
68+
{
69+
columnNames: ['namespace_id'],
70+
referencedTableName: 'namespaces',
71+
referencedColumnNames: ['id'],
72+
},
73+
{
74+
columnNames: ['resource_id'],
75+
referencedTableName: 'resources',
76+
referencedColumnNames: ['id'],
77+
},
78+
],
79+
});
80+
await queryRunner.createTable(table, true, true, true);
81+
}
82+
83+
export class Shares1753866547335 implements MigrationInterface {
84+
public async up(queryRunner: QueryRunner): Promise<void> {
85+
await createShareTypeEnum(queryRunner);
86+
await createSharesTable(queryRunner);
87+
}
88+
89+
public down(): Promise<void> {
90+
throw new Error('Not supported.');
91+
}
92+
}

src/shares/dto/share-info.dto.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Share, ShareType } from '../entities/share.entity';
2+
3+
export class ShareInfoDto {
4+
id: string;
5+
namespaceId: string;
6+
resourceId: string;
7+
enabled: boolean;
8+
requireLogin: boolean;
9+
passwordEnabled: boolean;
10+
shareType: ShareType;
11+
expiresAt: Date | null;
12+
13+
static new(namespaceId: string, resourceId: string): ShareInfoDto {
14+
const dto = new ShareInfoDto();
15+
dto.id = '';
16+
dto.namespaceId = namespaceId;
17+
dto.resourceId = resourceId;
18+
dto.enabled = false;
19+
dto.requireLogin = false;
20+
dto.passwordEnabled = false;
21+
dto.shareType = ShareType.ALL;
22+
dto.expiresAt = null;
23+
return dto;
24+
}
25+
26+
static fromEntity(share: Share): ShareInfoDto {
27+
const dto = new ShareInfoDto();
28+
dto.id = share.id;
29+
dto.namespaceId = share.namespaceId;
30+
dto.resourceId = share.resourceId;
31+
dto.enabled = share.enabled;
32+
dto.requireLogin = share.requireLogin;
33+
dto.passwordEnabled = !!share.password;
34+
dto.shareType = share.shareType;
35+
dto.expiresAt = share.expiresAt;
36+
return dto;
37+
}
38+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ShareType } from '../entities/share.entity';
2+
3+
export class UpdateShareInfoReqDto {
4+
enabled?: boolean;
5+
requireLogin?: boolean;
6+
password?: string | null;
7+
shareType?: ShareType;
8+
expiresAt?: Date | null;
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Base } from 'omniboxd/common/base.entity';
2+
import generateId from 'omniboxd/utils/generate-id';
3+
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
4+
5+
export enum ShareType {
6+
DOC_ONLY = 'doc_only',
7+
CHAT_ONLY = 'chat_only',
8+
ALL = 'all',
9+
}
10+
11+
@Entity('shares')
12+
export class Share extends Base {
13+
@PrimaryColumn()
14+
id: string;
15+
16+
@BeforeInsert()
17+
generateId?() {
18+
this.id = generateId(6);
19+
}
20+
21+
@Column()
22+
namespaceId: string;
23+
24+
@Column()
25+
resourceId: string;
26+
27+
@Column()
28+
enabled: boolean;
29+
30+
@Column()
31+
requireLogin: boolean;
32+
33+
@Column('enum', { enum: ShareType })
34+
shareType: ShareType;
35+
36+
@Column('varchar', { nullable: true })
37+
password: string | null;
38+
39+
@Column('timestamptz', { nullable: true })
40+
expiresAt: Date | null;
41+
}

src/shares/shares.controller.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Body, Controller, Get, Param, Patch, Req } from '@nestjs/common';
2+
import { SharesService } from './shares.service';
3+
import { UpdateShareInfoReqDto } from './dto/update-share-info-req.dto';
4+
5+
@Controller('api/v1/namespaces/:namespaceId/resources/:resourceId/share')
6+
export class SharesController {
7+
constructor(private readonly sharesService: SharesService) {}
8+
9+
@Get()
10+
async getShareInfo(
11+
@Req() req,
12+
@Param('namespaceId') namespaceId: string,
13+
@Param('resourceId') resourceId: string,
14+
) {
15+
return await this.sharesService.getShareInfo(namespaceId, resourceId);
16+
}
17+
18+
@Patch()
19+
async updateShareInfo(
20+
@Req() req,
21+
@Param('namespaceId') namespaceId: string,
22+
@Param('resourceId') resourceId: string,
23+
@Body() updateReq: UpdateShareInfoReqDto,
24+
) {
25+
return await this.sharesService.updateShareInfo(
26+
namespaceId,
27+
resourceId,
28+
updateReq,
29+
);
30+
}
31+
}

src/shares/shares.e2e-spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { TestClient } from 'test/test-client';
2+
3+
describe('SharesController (e2e)', () => {
4+
let client: TestClient;
5+
6+
beforeAll(async () => {
7+
client = await TestClient.create();
8+
});
9+
10+
afterAll(async () => {
11+
await client.close();
12+
});
13+
14+
it('update and get share info', async () => {
15+
const password = 'test-password';
16+
let res = await client
17+
.patch(
18+
`/api/v1/namespaces/${client.namespace.id}/resources/${client.namespace.root_resource_id}/share`,
19+
)
20+
.send({
21+
enabled: true,
22+
password,
23+
});
24+
expect(res.status).toBe(200);
25+
26+
res = await client.get(
27+
`/api/v1/namespaces/${client.namespace.id}/resources/${client.namespace.root_resource_id}/share`,
28+
);
29+
expect(res.status).toBe(200);
30+
expect(res.body.namespace_id).toBe(client.namespace.id);
31+
expect(res.body.resource_id).toBe(client.namespace.root_resource_id);
32+
expect(res.body.enabled).toBe(true);
33+
expect(res.body.password_enabled).toBe(true);
34+
});
35+
});

src/shares/shares.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { SharesService } from './shares.service';
3+
import { TypeOrmModule } from '@nestjs/typeorm';
4+
import { Share } from './entities/share.entity';
5+
import { SharesController } from './shares.controller';
6+
7+
@Module({
8+
imports: [TypeOrmModule.forFeature([Share])],
9+
providers: [SharesService],
10+
controllers: [SharesController],
11+
})
12+
export class SharesModule {}

src/shares/shares.service.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Share, ShareType } from './entities/share.entity';
4+
import { Repository } from 'typeorm';
5+
import { ShareInfoDto } from './dto/share-info.dto';
6+
import { UpdateShareInfoReqDto } from './dto/update-share-info-req.dto';
7+
8+
@Injectable()
9+
export class SharesService {
10+
constructor(
11+
@InjectRepository(Share)
12+
private readonly shareRepo: Repository<Share>,
13+
) {}
14+
15+
async getShareInfo(
16+
namespaceId: string,
17+
resourceId: string,
18+
): Promise<ShareInfoDto> {
19+
const share = await this.shareRepo.findOne({
20+
where: {
21+
namespaceId,
22+
resourceId,
23+
},
24+
});
25+
if (!share) {
26+
return ShareInfoDto.new(namespaceId, resourceId);
27+
}
28+
return ShareInfoDto.fromEntity(share);
29+
}
30+
31+
async updateShareInfo(
32+
namespaceId: string,
33+
resourceId: string,
34+
req: UpdateShareInfoReqDto,
35+
): Promise<ShareInfoDto> {
36+
let share = await this.shareRepo.findOne({
37+
where: {
38+
namespaceId,
39+
resourceId,
40+
},
41+
});
42+
if (!share) {
43+
share = this.shareRepo.create({
44+
namespaceId,
45+
resourceId,
46+
enabled: false,
47+
requireLogin: true,
48+
shareType: ShareType.ALL,
49+
password: null,
50+
expiresAt: null,
51+
});
52+
}
53+
if (req.enabled !== undefined) {
54+
share.enabled = req.enabled;
55+
}
56+
if (req.requireLogin !== undefined) {
57+
share.requireLogin = req.requireLogin;
58+
}
59+
if (req.password !== undefined) {
60+
share.password = req.password;
61+
}
62+
if (req.shareType !== undefined) {
63+
share.shareType = req.shareType;
64+
}
65+
if (req.expiresAt !== undefined) {
66+
share.expiresAt = req.expiresAt;
67+
}
68+
const savedShare = await this.shareRepo.save(share);
69+
return ShareInfoDto.fromEntity(savedShare);
70+
}
71+
}

0 commit comments

Comments
 (0)