feat(server): init user module (#2018)

This commit is contained in:
Himself65
2023-04-18 18:14:25 -05:00
committed by GitHub
parent c6be29f944
commit 3a053af50c
16 changed files with 540 additions and 9 deletions

View File

@@ -195,13 +195,45 @@ jobs:
name: Server Test
runs-on: ubuntu-latest
environment: development
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn exec prisma generate
yarn exec prisma db push
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn exec ts-node-esm ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run server tests
run: yarn test:coverage
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:

View File

@@ -86,13 +86,46 @@ jobs:
name: Server Test
runs-on: ubuntu-latest
environment: development
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn exec prisma generate
yarn exec prisma db push
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn exec ts-node-esm ./scripts/init-db.ts
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run server tests
run: yarn test:coverage
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:

View File

@@ -0,0 +1,67 @@
-- CreateTable
CREATE TABLE "google_users" (
"id" VARCHAR NOT NULL,
"user_id" VARCHAR NOT NULL,
"google_id" VARCHAR NOT NULL,
CONSTRAINT "google_users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permissions" (
"id" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"user_id" VARCHAR,
"user_email" TEXT,
"type" SMALLINT NOT NULL,
"accepted" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "seaql_migrations" (
"version" VARCHAR NOT NULL,
"applied_at" BIGINT NOT NULL,
CONSTRAINT "seaql_migrations_pkey" PRIMARY KEY ("version")
);
-- CreateTable
CREATE TABLE "users" (
"id" VARCHAR NOT NULL,
"name" VARCHAR NOT NULL,
"email" VARCHAR NOT NULL,
"avatar_url" VARCHAR,
"token_nonce" SMALLINT DEFAULT 0,
"password" VARCHAR,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workspaces" (
"id" VARCHAR NOT NULL,
"public" BOOLEAN NOT NULL,
"type" SMALLINT NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "google_users_google_id_key" ON "google_users"("google_id");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "google_users" ADD CONSTRAINT "google_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "permissions" ADD CONSTRAINT "permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "permissions" ADD CONSTRAINT "permissions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -4,10 +4,13 @@
"version": "0.5.3",
"description": "Affine Node.js server",
"type": "module",
"bin": {
"run-test": "./scripts/run-test.ts"
},
"scripts": {
"dev": "nodemon ./src/index.ts",
"test": "NODE_ENV=test node --loader ts-node/esm.mjs --es-module-specifier-resolution node --test ./src/tests/*",
"test:coverage": "NODE_ENV=test c8 node --loader ts-node/esm.mjs --es-module-specifier-resolution node --experimental-test-coverage ./src/tests/*"
"test": "ts-node-esm ./scripts/run-test.ts all",
"test:coverage": "c8 ts-node-esm ./scripts/run-test.ts all"
},
"dependencies": {
"@apollo/server": "^4.6.0",
@@ -29,8 +32,10 @@
"@nestjs/testing": "^9.4.0",
"@types/lodash-es": "^4.14.194",
"@types/node": "^18.15.11",
"@types/supertest": "^2.0.12",
"c8": "^7.13.0",
"nodemon": "^2.0.22",
"supertest": "^6.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"vitest": "^0.30.1"
@@ -64,6 +69,7 @@
],
"report-dir": ".coverage",
"exclude": [
"scripts",
"node_modules",
"**/*.spec.ts"
]

View File

@@ -0,0 +1,24 @@
import { randomUUID } from 'node:crypto';
import userA from '@affine-test/fixtures/userA.json' assert { type: 'json' };
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.users.create({
data: {
id: randomUUID(),
...userA,
},
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async e => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

56
apps/server/scripts/run-test.ts Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env ts-node-esm
import { resolve } from 'node:path';
import * as p from '@clack/prompts';
import { spawn } from 'child_process';
import { readdir } from 'fs/promises';
import * as process from 'process';
import { fileURLToPath } from 'url';
import pkg from '../package.json' assert { type: 'json' };
const root = fileURLToPath(new URL('..', import.meta.url));
const testDir = resolve(root, 'src', 'tests');
const files = await readdir(testDir);
const args = [...pkg.nodemonConfig.nodeArgs, '--test'];
const env = {
PATH: process.env.PATH,
NODE_ENV: 'test',
DATABASE_URL: process.env.DATABASE_URL,
};
if (process.argv[2] === 'all') {
const cp = spawn('node', [...args, resolve(testDir, '*')], {
cwd: root,
env,
stdio: 'inherit',
shell: true,
});
cp.on('exit', code => {
process.exit(code ?? 0);
});
} else {
const result = await p.group({
file: () =>
p.select({
message: 'Select a file to run',
options: files.map(file => ({
label: file,
value: file as any,
})),
}),
});
const target = resolve(testDir, result.file);
const cp = spawn('node', [...args, target], {
cwd: root,
env,
stdio: 'inherit',
shell: true,
});
cp.on('exit', code => {
process.exit(code ?? 0);
});
}

View File

@@ -19,7 +19,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
},
https: false,
host: 'localhost',
port: 3000,
port: 3010,
path: '',
get origin() {
return this.dev

View File

@@ -1,3 +1,4 @@
import { UsersModule } from './users';
import { WorkspaceModule } from './workspaces';
export const BusinessModules = [WorkspaceModule];
export const BusinessModules = [WorkspaceModule, UsersModule];

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { UserResolver } from './resolver';
@Module({
providers: [UserResolver],
})
export class UsersModule {}

View File

@@ -0,0 +1,37 @@
import { Args, Field, ID, ObjectType, Query, Resolver } from '@nestjs/graphql';
import type { users } from '@prisma/client';
import { PrismaService } from '../../prisma/service';
@ObjectType()
export class User implements users {
@Field(() => ID)
id!: string;
@Field({ description: 'User name' })
name!: string;
@Field({ description: 'User email' })
email!: string;
@Field({ description: 'User password', nullable: true })
password!: string;
@Field({ description: 'User avatar url', nullable: true })
avatar_url!: string;
@Field({ description: 'User token nonce', nullable: true })
token_nonce!: number;
@Field({ description: 'User created date', nullable: true })
created_at!: Date;
}
@Resolver(() => User)
export class UserResolver {
constructor(private readonly prisma: PrismaService) {}
@Query(() => User, {
name: 'user',
description: 'Get user by email',
})
async user(@Args('email') email: string) {
return this.prisma.users.findUnique({
where: { email },
});
}
}

View File

@@ -1,6 +1,10 @@
import { randomUUID } from 'node:crypto';
import {
Args,
Field,
ID,
Mutation,
ObjectType,
Query,
registerEnumType,
@@ -17,11 +21,20 @@ export enum WorkspaceType {
registerEnumType(WorkspaceType, {
name: 'WorkspaceType',
description: 'Workspace type',
valuesMap: {
Normal: {
description: 'Normal workspace',
},
Private: {
description: 'Private workspace',
},
},
});
@ObjectType()
export class Workspace implements workspaces {
@Field()
@Field(() => ID)
id!: string;
@Field({ description: 'is Public workspace' })
public!: boolean;
@@ -53,4 +66,20 @@ export class WorkspaceResolver {
where: { id },
});
}
// create workspace
@Mutation(() => Workspace, {
name: 'createWorkspace',
description: 'Create workspace',
})
async createWorkspace() {
return this.prisma.workspaces.create({
data: {
id: randomUUID(),
type: WorkspaceType.Private,
public: false,
created_at: new Date(),
},
});
}
}

View File

@@ -0,0 +1,100 @@
import { equal, ok } from 'node:assert';
import { afterEach, beforeEach, describe, test } from 'node:test';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../app';
import { getDefaultAFFiNEConfig } from '../config/default';
const gql = '/graphql';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
// please run `ts-node-esm ./scripts/init-db.ts` before running this test
describe('AppModule', () => {
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
test('should init app', async () => {
ok(typeof app === 'object');
await request(app.getHttpServer())
.post(gql)
.send({
query: `
query {
error
}
`,
})
.expect(400);
await request(app.getHttpServer())
.post(gql)
.send({
query: `
mutation {
createWorkspace {
id
type
public
created_at
}
}
`,
})
.expect(200)
.expect(res => {
ok(
typeof res.body.data.createWorkspace === 'object',
'res.body.data.createWorkspace is not an object'
);
ok(
typeof res.body.data.createWorkspace.id === 'string',
'res.body.data.createWorkspace.id is not a string'
);
ok(
typeof res.body.data.createWorkspace.type === 'string',
'res.body.data.createWorkspace.type is not a string'
);
ok(
typeof res.body.data.createWorkspace.public === 'boolean',
'res.body.data.createWorkspace.public is not a boolean'
);
ok(
typeof res.body.data.createWorkspace.created_at === 'string',
'res.body.data.createWorkspace.created_at is not a string'
);
});
});
test('should find default user', async () => {
await request(app.getHttpServer())
.post(gql)
.send({
query: `
query {
user(email: "alex.yang@example.org") {
email
avatar_url
}
}
`,
})
.expect(200)
.expect(res => {
equal(res.body.data.user.email, 'alex.yang@example.org');
});
});
});

View File

@@ -15,6 +15,11 @@
},
"include": ["src", "package.json"],
"exclude": ["dist", "node_modules"],
"references": [
{
"path": "./tsconfig.node.json"
}
],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"resolveJsonModule": true,
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["./scripts", "package.json"]
}

127
yarn.lock
View File

@@ -214,6 +214,7 @@ __metadata:
"@prisma/client": ^4.12.0
"@types/lodash-es": ^4.14.194
"@types/node": ^18.15.11
"@types/supertest": ^2.0.12
c8: ^7.13.0
dotenv: ^16.0.3
graphql: ^16.6.0
@@ -223,9 +224,12 @@ __metadata:
prisma: ^4.12.0
reflect-metadata: ^0.1.13
rxjs: ^7.8.0
supertest: ^6.3.3
ts-node: ^10.9.1
typescript: ^5.0.4
vitest: ^0.30.1
bin:
run-test: ./scripts/run-test.ts
languageName: unknown
linkType: soft
@@ -7545,6 +7549,13 @@ __metadata:
languageName: node
linkType: hard
"@types/cookiejar@npm:*":
version: 2.1.2
resolution: "@types/cookiejar@npm:2.1.2"
checksum: f6e1903454007f86edd6c3520cbb4d553e1d4e17eaf1f77f6f75e3270f48cc828d74397a113a36942f5fe52f9fa71067bcfa738f53ad468fcca0bc52cb1cbd28
languageName: node
linkType: hard
"@types/debug@npm:^4.1.7":
version: 4.1.7
resolution: "@types/debug@npm:4.1.7"
@@ -8046,6 +8057,25 @@ __metadata:
languageName: node
linkType: hard
"@types/superagent@npm:*":
version: 4.1.16
resolution: "@types/superagent@npm:4.1.16"
dependencies:
"@types/cookiejar": "*"
"@types/node": "*"
checksum: 187d1d32fdafd20b27e81728c46283160d3296ad904d56e0780769cf524105c94cc64bf5bafa170400cf5f1063d30826427de42ff0894d15b54df6d0fa31be4e
languageName: node
linkType: hard
"@types/supertest@npm:^2.0.12":
version: 2.0.12
resolution: "@types/supertest@npm:2.0.12"
dependencies:
"@types/superagent": "*"
checksum: f0e2b44f86bec2f708d6a3d0cb209055b487922040773049b0f8c6b557af52d4b5fa904e17dfaa4ce6e610172206bbec7b62420d158fa57b6ffc2de37b1730d3
languageName: node
linkType: hard
"@types/testing-library__jest-dom@npm:^5.9.1":
version: 5.14.5
resolution: "@types/testing-library__jest-dom@npm:5.14.5"
@@ -9201,6 +9231,13 @@ __metadata:
languageName: node
linkType: hard
"asap@npm:^2.0.0":
version: 2.0.6
resolution: "asap@npm:2.0.6"
checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d
languageName: node
linkType: hard
"asar@npm:^3.0.0":
version: 3.2.0
resolution: "asar@npm:3.2.0"
@@ -10579,6 +10616,13 @@ __metadata:
languageName: node
linkType: hard
"component-emitter@npm:^1.3.0":
version: 1.3.0
resolution: "component-emitter@npm:1.3.0"
checksum: b3c46de38ffd35c57d1c02488355be9f218e582aec72d72d1b8bbec95a3ac1b38c96cd6e03ff015577e68f550fbb361a3bfdbd9bb248be9390b7b3745691be6b
languageName: node
linkType: hard
"compressible@npm:~2.0.16":
version: 2.0.18
resolution: "compressible@npm:2.0.18"
@@ -10767,6 +10811,13 @@ __metadata:
languageName: node
linkType: hard
"cookiejar@npm:^2.1.4":
version: 2.1.4
resolution: "cookiejar@npm:2.1.4"
checksum: c4442111963077dc0e5672359956d6556a195d31cbb35b528356ce5f184922b99ac48245ac05ed86cf993f7df157c56da10ab3efdadfed79778a0d9b1b092d5b
languageName: node
linkType: hard
"copy-anything@npm:^3.0.2":
version: 3.0.3
resolution: "copy-anything@npm:3.0.3"
@@ -11338,6 +11389,16 @@ __metadata:
languageName: node
linkType: hard
"dezalgo@npm:^1.0.4":
version: 1.0.4
resolution: "dezalgo@npm:1.0.4"
dependencies:
asap: ^2.0.0
wrappy: 1
checksum: 895389c6aead740d2ab5da4d3466d20fa30f738010a4d3f4dcccc9fc645ca31c9d10b7e1804ae489b1eb02c7986f9f1f34ba132d409b043082a86d9a4e745624
languageName: node
linkType: hard
"diff-sequences@npm:^28.1.1":
version: 28.1.1
resolution: "diff-sequences@npm:28.1.1"
@@ -12902,7 +12963,7 @@ __metadata:
languageName: node
linkType: hard
"fast-safe-stringify@npm:2.1.1":
"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.1.1":
version: 2.1.1
resolution: "fast-safe-stringify@npm:2.1.1"
checksum: a851cbddc451745662f8f00ddb622d6766f9bd97642dabfd9a405fb0d646d69fc0b9a1243cbf67f5f18a39f40f6fa821737651ff1bceeba06c9992ca2dc5bd3d
@@ -13330,6 +13391,18 @@ __metadata:
languageName: node
linkType: hard
"formidable@npm:^2.1.2":
version: 2.1.2
resolution: "formidable@npm:2.1.2"
dependencies:
dezalgo: ^1.0.4
hexoid: ^1.0.0
once: ^1.4.0
qs: ^6.11.0
checksum: 81c8e5d89f5eb873e992893468f0de22c01678ca3d315db62be0560f9de1c77d4faefc9b1f4575098eb2263b3c81ba1024833a9fc3206297ddbac88a4f69b7a8
languageName: node
linkType: hard
"forwarded@npm:0.2.0":
version: 0.2.0
resolution: "forwarded@npm:0.2.0"
@@ -14275,6 +14348,13 @@ __metadata:
languageName: node
linkType: hard
"hexoid@npm:^1.0.0":
version: 1.0.0
resolution: "hexoid@npm:1.0.0"
checksum: 27a148ca76a2358287f40445870116baaff4a0ed0acc99900bf167f0f708ffd82e044ff55e9949c71963852b580fc024146d3ac6d5d76b508b78d927fa48ae2d
languageName: node
linkType: hard
"highlight.js@npm:^10.4.1, highlight.js@npm:~10.7.0":
version: 10.7.3
resolution: "highlight.js@npm:10.7.3"
@@ -17229,7 +17309,7 @@ __metadata:
languageName: node
linkType: hard
"methods@npm:~1.1.2":
"methods@npm:^1.1.2, methods@npm:~1.1.2":
version: 1.1.2
resolution: "methods@npm:1.1.2"
checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a
@@ -17287,7 +17367,7 @@ __metadata:
languageName: node
linkType: hard
"mime@npm:^2.0.3":
"mime@npm:2.6.0, mime@npm:^2.0.3":
version: 2.6.0
resolution: "mime@npm:2.6.0"
bin:
@@ -19205,7 +19285,7 @@ __metadata:
languageName: node
linkType: hard
"qs@npm:^6.10.0":
"qs@npm:^6.10.0, qs@npm:^6.11.0":
version: 6.11.1
resolution: "qs@npm:6.11.1"
dependencies:
@@ -20424,6 +20504,17 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.3.8":
version: 7.5.0
resolution: "semver@npm:7.5.0"
dependencies:
lru-cache: ^6.0.0
bin:
semver: bin/semver.js
checksum: 2d266937756689a76f124ffb4c1ea3e1bbb2b263219f90ada8a11aebebe1280b13bb76cca2ca96bdee3dbc554cbc0b24752eb895b2a51577aa644427e9229f2b
languageName: node
linkType: hard
"semver@npm:~7.0.0":
version: 7.0.0
resolution: "semver@npm:7.0.0"
@@ -21387,6 +21478,24 @@ __metadata:
languageName: node
linkType: hard
"superagent@npm:^8.0.5":
version: 8.0.9
resolution: "superagent@npm:8.0.9"
dependencies:
component-emitter: ^1.3.0
cookiejar: ^2.1.4
debug: ^4.3.4
fast-safe-stringify: ^2.1.1
form-data: ^4.0.0
formidable: ^2.1.2
methods: ^1.1.2
mime: 2.6.0
qs: ^6.11.0
semver: ^7.3.8
checksum: 5d00cdc7ceb5570663da80604965750e6b1b8d7d7442b7791e285c62bcd8d578a8ead0242a2426432b59a255fb42eb3a196d636157538a1392e7b6c5f1624810
languageName: node
linkType: hard
"superjson@npm:^1.12.2":
version: 1.12.2
resolution: "superjson@npm:1.12.2"
@@ -21396,6 +21505,16 @@ __metadata:
languageName: node
linkType: hard
"supertest@npm:^6.3.3":
version: 6.3.3
resolution: "supertest@npm:6.3.3"
dependencies:
methods: ^1.1.2
superagent: ^8.0.5
checksum: 38239e517f7ba62b7a139a79c5c48d55f8d67b5ff4b6e51d5b07732ca8bbc4a28ffa1b10916fbb403dd013a054dbf028edc5850057d9a43aecbff439d494673e
languageName: node
linkType: hard
"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0":
version: 5.5.0
resolution: "supports-color@npm:5.5.0"