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
@@ -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;
@@ -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"
+8 -2
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"
]
+24
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
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);
});
}
+1 -1
View File
@@ -19,7 +19,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
},
https: false,
host: 'localhost',
port: 3000,
port: 3010,
path: '',
get origin() {
return this.dev
+2 -1
View File
@@ -1,3 +1,4 @@
import { UsersModule } from './users';
import { WorkspaceModule } from './workspaces';
export const BusinessModules = [WorkspaceModule];
export const BusinessModules = [WorkspaceModule, UsersModule];
+8
View File
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { UserResolver } from './resolver';
@Module({
providers: [UserResolver],
})
export class UsersModule {}
+37
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 },
});
}
}
+30 -1
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(),
},
});
}
}
+100
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');
});
});
});
+5
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"
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"resolveJsonModule": true,
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["./scripts", "package.json"]
}