Add tsoa create endpoints for orgs users and projects
This commit is contained in:
@@ -56,6 +56,7 @@ The data is intentionally uneven across orgs so consumers can test:
|
|||||||
|
|
||||||
### Organizations
|
### Organizations
|
||||||
|
|
||||||
|
- `POST /orgs`
|
||||||
- `GET /orgs`
|
- `GET /orgs`
|
||||||
- `GET /orgs/:orgId`
|
- `GET /orgs/:orgId`
|
||||||
- `GET /orgs/:orgId/projects`
|
- `GET /orgs/:orgId/projects`
|
||||||
@@ -63,8 +64,10 @@ The data is intentionally uneven across orgs so consumers can test:
|
|||||||
|
|
||||||
### Users and Projects
|
### Users and Projects
|
||||||
|
|
||||||
|
- `POST /users`
|
||||||
- `GET /users`
|
- `GET /users`
|
||||||
- `GET /users?orgId=org_acme`
|
- `GET /users?orgId=org_acme`
|
||||||
|
- `POST /projects`
|
||||||
- `GET /projects`
|
- `GET /projects`
|
||||||
- `GET /projects?orgId=org_northstar`
|
- `GET /projects?orgId=org_northstar`
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
import type { Request as ExRequest } from "express";
|
import type { Request as ExRequest } from "express";
|
||||||
import { Controller, Get, Path, Query, Request, Response, Route, Tags } from "tsoa";
|
import {
|
||||||
import { getOrganizationSummary, listOrganizations, listProjects, listTasks } from "../repository";
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Path,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
Route,
|
||||||
|
SuccessResponse,
|
||||||
|
Tags,
|
||||||
|
} from "tsoa";
|
||||||
|
import {
|
||||||
|
createOrganization,
|
||||||
|
getOrganizationSummary,
|
||||||
|
listOrganizations,
|
||||||
|
listProjects,
|
||||||
|
listTasks,
|
||||||
|
} from "../repository";
|
||||||
import type {
|
import type {
|
||||||
|
CreateOrganizationRequest,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
OrganizationEnvelope,
|
OrganizationEnvelope,
|
||||||
OrganizationsEnvelope,
|
OrganizationsEnvelope,
|
||||||
@@ -9,7 +28,7 @@ import type {
|
|||||||
TasksEnvelope,
|
TasksEnvelope,
|
||||||
} from "../models";
|
} from "../models";
|
||||||
import { HttpError } from "../httpErrors";
|
import { HttpError } from "../httpErrors";
|
||||||
import { getDatabase } from "./shared";
|
import { getDatabase, normalizeCreateOrganization } from "./shared";
|
||||||
|
|
||||||
@Route("orgs")
|
@Route("orgs")
|
||||||
@Tags("Organizations")
|
@Tags("Organizations")
|
||||||
@@ -19,6 +38,26 @@ export class OrganizationsController extends Controller {
|
|||||||
return { organizations: listOrganizations(getDatabase(request)) };
|
return { organizations: listOrganizations(getDatabase(request)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@SuccessResponse("201", "Created")
|
||||||
|
@Response<ErrorResponse>(400, "Invalid payload")
|
||||||
|
public createOrganization(
|
||||||
|
@Request() request: ExRequest,
|
||||||
|
@Body() body: CreateOrganizationRequest,
|
||||||
|
): OrganizationEnvelope {
|
||||||
|
this.setStatus(201);
|
||||||
|
const organization = createOrganization(
|
||||||
|
getDatabase(request),
|
||||||
|
normalizeCreateOrganization(body),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
throw new HttpError(500, "Failed to create organization.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { organization };
|
||||||
|
}
|
||||||
|
|
||||||
@Get("{orgId}")
|
@Get("{orgId}")
|
||||||
@Response<ErrorResponse>(404, "Organization not found")
|
@Response<ErrorResponse>(404, "Organization not found")
|
||||||
public getOrganization(
|
public getOrganization(
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { Request as ExRequest } from "express";
|
import type { Request as ExRequest } from "express";
|
||||||
import { Get, Query, Request, Route, Tags } from "tsoa";
|
import { Body, Controller, Get, Post, Query, Request, Response, Route, SuccessResponse, Tags } from "tsoa";
|
||||||
import { listProjects } from "../repository";
|
import { createProject, listProjects } from "../repository";
|
||||||
import type { ProjectsEnvelope } from "../models";
|
import type { CreateProjectRequest, ErrorResponse, ProjectEnvelope, ProjectsEnvelope } from "../models";
|
||||||
import { getDatabase } from "./shared";
|
import { HttpError } from "../httpErrors";
|
||||||
|
import { getDatabase, normalizeCreateProject } from "./shared";
|
||||||
|
|
||||||
@Route("projects")
|
@Route("projects")
|
||||||
@Tags("Projects")
|
@Tags("Projects")
|
||||||
export class ProjectsController {
|
export class ProjectsController extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
public listProjects(
|
public listProjects(
|
||||||
@Request() request: ExRequest,
|
@Request() request: ExRequest,
|
||||||
@@ -14,4 +15,20 @@ export class ProjectsController {
|
|||||||
): ProjectsEnvelope {
|
): ProjectsEnvelope {
|
||||||
return { projects: listProjects(getDatabase(request), orgId) };
|
return { projects: listProjects(getDatabase(request), orgId) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@SuccessResponse("201", "Created")
|
||||||
|
@Response<ErrorResponse>(400, "Invalid payload")
|
||||||
|
public createProject(
|
||||||
|
@Request() request: ExRequest,
|
||||||
|
@Body() body: CreateProjectRequest,
|
||||||
|
): ProjectEnvelope {
|
||||||
|
this.setStatus(201);
|
||||||
|
const project = createProject(getDatabase(request), normalizeCreateProject(body));
|
||||||
|
if (!project) {
|
||||||
|
throw new HttpError(500, "Failed to create project.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { project };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { Request as ExRequest } from "express";
|
import type { Request as ExRequest } from "express";
|
||||||
import { Get, Query, Request, Route, Tags } from "tsoa";
|
import { Body, Controller, Get, Post, Query, Request, Response, Route, SuccessResponse, Tags } from "tsoa";
|
||||||
import { listUsers } from "../repository";
|
import { createUser, listUsers } from "../repository";
|
||||||
import type { UsersEnvelope } from "../models";
|
import type { CreateUserRequest, ErrorResponse, UserEnvelope, UsersEnvelope } from "../models";
|
||||||
import { getDatabase } from "./shared";
|
import { HttpError } from "../httpErrors";
|
||||||
|
import { getDatabase, normalizeCreateUser } from "./shared";
|
||||||
|
|
||||||
@Route("users")
|
@Route("users")
|
||||||
@Tags("Users")
|
@Tags("Users")
|
||||||
export class UsersController {
|
export class UsersController extends Controller {
|
||||||
@Get()
|
@Get()
|
||||||
public listUsers(
|
public listUsers(
|
||||||
@Request() request: ExRequest,
|
@Request() request: ExRequest,
|
||||||
@@ -14,4 +15,20 @@ export class UsersController {
|
|||||||
): UsersEnvelope {
|
): UsersEnvelope {
|
||||||
return { users: listUsers(getDatabase(request), orgId) };
|
return { users: listUsers(getDatabase(request), orgId) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@SuccessResponse("201", "Created")
|
||||||
|
@Response<ErrorResponse>(400, "Invalid payload")
|
||||||
|
public createUser(
|
||||||
|
@Request() request: ExRequest,
|
||||||
|
@Body() body: CreateUserRequest,
|
||||||
|
): UserEnvelope {
|
||||||
|
this.setStatus(201);
|
||||||
|
const user = createUser(getDatabase(request), normalizeCreateUser(body));
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpError(500, "Failed to create user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import type { Database } from "bun:sqlite";
|
import type { Database } from "bun:sqlite";
|
||||||
import type { Request as ExRequest } from "express";
|
import type { Request as ExRequest } from "express";
|
||||||
import { HttpError } from "../httpErrors";
|
import { HttpError } from "../httpErrors";
|
||||||
import type { CreateTaskRequest, UpdateTaskRequest } from "../models";
|
import type {
|
||||||
|
CreateOrganizationRequest,
|
||||||
|
CreateProjectRequest,
|
||||||
|
CreateTaskRequest,
|
||||||
|
CreateUserRequest,
|
||||||
|
UpdateTaskRequest,
|
||||||
|
} from "../models";
|
||||||
|
|
||||||
export function getDatabase(request: ExRequest): Database {
|
export function getDatabase(request: ExRequest): Database {
|
||||||
return request.app.locals.db as Database;
|
return request.app.locals.db as Database;
|
||||||
@@ -11,6 +17,14 @@ function optionalText(value: unknown) {
|
|||||||
return typeof value === "string" ? value.trim() : undefined;
|
return typeof value === "string" ? value.trim() : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requiredText(value: unknown, fieldName: string) {
|
||||||
|
if (typeof value !== "string" || value.trim() === "") {
|
||||||
|
throw new HttpError(400, `${fieldName} is required.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function nullableText(value: unknown) {
|
function nullableText(value: unknown) {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
return value.trim();
|
return value.trim();
|
||||||
@@ -28,22 +42,12 @@ function nullableInteger(value: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCreateTask(body: CreateTaskRequest): CreateTaskRequest {
|
export function normalizeCreateTask(body: CreateTaskRequest): CreateTaskRequest {
|
||||||
if (typeof body.orgId !== "string" || body.orgId.trim() === "") {
|
|
||||||
throw new HttpError(400, "orgId is required.");
|
|
||||||
}
|
|
||||||
if (typeof body.projectId !== "string" || body.projectId.trim() === "") {
|
|
||||||
throw new HttpError(400, "projectId is required.");
|
|
||||||
}
|
|
||||||
if (typeof body.title !== "string" || body.title.trim() === "") {
|
|
||||||
throw new HttpError(400, "title is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orgId: body.orgId.trim(),
|
orgId: requiredText(body.orgId, "orgId"),
|
||||||
projectId: body.projectId.trim(),
|
projectId: requiredText(body.projectId, "projectId"),
|
||||||
assigneeUserId:
|
assigneeUserId:
|
||||||
typeof body.assigneeUserId === "string" ? body.assigneeUserId.trim() : null,
|
typeof body.assigneeUserId === "string" ? body.assigneeUserId.trim() : null,
|
||||||
title: body.title.trim(),
|
title: requiredText(body.title, "title"),
|
||||||
description: optionalText(body.description),
|
description: optionalText(body.description),
|
||||||
status: optionalText(body.status),
|
status: optionalText(body.status),
|
||||||
priority: optionalText(body.priority),
|
priority: optionalText(body.priority),
|
||||||
@@ -57,6 +61,43 @@ export function normalizeCreateTask(body: CreateTaskRequest): CreateTaskRequest
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeCreateOrganization(
|
||||||
|
body: CreateOrganizationRequest,
|
||||||
|
): CreateOrganizationRequest {
|
||||||
|
return {
|
||||||
|
slug: requiredText(body.slug, "slug"),
|
||||||
|
name: requiredText(body.name, "name"),
|
||||||
|
plan: requiredText(body.plan, "plan"),
|
||||||
|
industry: requiredText(body.industry, "industry"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCreateUser(body: CreateUserRequest): CreateUserRequest {
|
||||||
|
return {
|
||||||
|
orgId: requiredText(body.orgId, "orgId"),
|
||||||
|
fullName: requiredText(body.fullName, "fullName"),
|
||||||
|
email: requiredText(body.email, "email"),
|
||||||
|
role: requiredText(body.role, "role"),
|
||||||
|
timezone: requiredText(body.timezone, "timezone"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCreateProject(body: CreateProjectRequest): CreateProjectRequest {
|
||||||
|
return {
|
||||||
|
orgId: requiredText(body.orgId, "orgId"),
|
||||||
|
key: requiredText(body.key, "key"),
|
||||||
|
name: requiredText(body.name, "name"),
|
||||||
|
status: requiredText(body.status, "status"),
|
||||||
|
ownerUserId: requiredText(body.ownerUserId, "ownerUserId"),
|
||||||
|
dueDate:
|
||||||
|
typeof body.dueDate === "string"
|
||||||
|
? body.dueDate.trim()
|
||||||
|
: body.dueDate === null
|
||||||
|
? null
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeUpdateTask(body: UpdateTaskRequest): UpdateTaskRequest {
|
export function normalizeUpdateTask(body: UpdateTaskRequest): UpdateTaskRequest {
|
||||||
return {
|
return {
|
||||||
title: optionalText(body.title),
|
title: optionalText(body.title),
|
||||||
|
|||||||
@@ -41,6 +41,40 @@ const models: TsoaRoute.Models = {
|
|||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
},
|
},
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
"UserEnvelope": {
|
||||||
|
"dataType": "refObject",
|
||||||
|
"properties": {
|
||||||
|
"user": {"ref":"User","required":true},
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
},
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
"Record_string.unknown_": {
|
||||||
|
"dataType": "refAlias",
|
||||||
|
"type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"any"},"validators":{}},
|
||||||
|
},
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
"ErrorResponse": {
|
||||||
|
"dataType": "refObject",
|
||||||
|
"properties": {
|
||||||
|
"error": {"dataType":"string","required":true},
|
||||||
|
"details": {"ref":"Record_string.unknown_"},
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
},
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
"CreateUserRequest": {
|
||||||
|
"dataType": "refObject",
|
||||||
|
"properties": {
|
||||||
|
"orgId": {"dataType":"string","required":true},
|
||||||
|
"fullName": {"dataType":"string","required":true},
|
||||||
|
"email": {"dataType":"string","required":true},
|
||||||
|
"role": {"dataType":"string","required":true},
|
||||||
|
"timezone": {"dataType":"string","required":true},
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
},
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
"TaskListItem": {
|
"TaskListItem": {
|
||||||
"dataType": "refObject",
|
"dataType": "refObject",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -125,20 +159,6 @@ const models: TsoaRoute.Models = {
|
|||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
},
|
},
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
"Record_string.unknown_": {
|
|
||||||
"dataType": "refAlias",
|
|
||||||
"type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"any"},"validators":{}},
|
|
||||||
},
|
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
|
||||||
"ErrorResponse": {
|
|
||||||
"dataType": "refObject",
|
|
||||||
"properties": {
|
|
||||||
"error": {"dataType":"string","required":true},
|
|
||||||
"details": {"ref":"Record_string.unknown_"},
|
|
||||||
},
|
|
||||||
"additionalProperties": true,
|
|
||||||
},
|
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
|
||||||
"CreateTaskRequest": {
|
"CreateTaskRequest": {
|
||||||
"dataType": "refObject",
|
"dataType": "refObject",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -193,6 +213,27 @@ const models: TsoaRoute.Models = {
|
|||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
},
|
},
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
"ProjectEnvelope": {
|
||||||
|
"dataType": "refObject",
|
||||||
|
"properties": {
|
||||||
|
"project": {"ref":"Project","required":true},
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
},
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
"CreateProjectRequest": {
|
||||||
|
"dataType": "refObject",
|
||||||
|
"properties": {
|
||||||
|
"orgId": {"dataType":"string","required":true},
|
||||||
|
"key": {"dataType":"string","required":true},
|
||||||
|
"name": {"dataType":"string","required":true},
|
||||||
|
"status": {"dataType":"string","required":true},
|
||||||
|
"ownerUserId": {"dataType":"string","required":true},
|
||||||
|
"dueDate": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]},
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
},
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
"OrganizationSummaryListItem": {
|
"OrganizationSummaryListItem": {
|
||||||
"dataType": "refObject",
|
"dataType": "refObject",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -252,6 +293,17 @@ const models: TsoaRoute.Models = {
|
|||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
},
|
},
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
"CreateOrganizationRequest": {
|
||||||
|
"dataType": "refObject",
|
||||||
|
"properties": {
|
||||||
|
"slug": {"dataType":"string","required":true},
|
||||||
|
"name": {"dataType":"string","required":true},
|
||||||
|
"plan": {"dataType":"string","required":true},
|
||||||
|
"industry": {"dataType":"string","required":true},
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
},
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
"HealthResponse": {
|
"HealthResponse": {
|
||||||
"dataType": "refObject",
|
"dataType": "refObject",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -308,6 +360,37 @@ export function RegisterRoutes(app: Router) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
const argsUsersController_createUser: Record<string, TsoaRoute.ParameterSchema> = {
|
||||||
|
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
||||||
|
body: {"in":"body","name":"body","required":true,"ref":"CreateUserRequest"},
|
||||||
|
};
|
||||||
|
app.post('/users',
|
||||||
|
...(fetchMiddlewares<RequestHandler>(UsersController)),
|
||||||
|
...(fetchMiddlewares<RequestHandler>(UsersController.prototype.createUser)),
|
||||||
|
|
||||||
|
async function UsersController_createUser(request: ExRequest, response: ExResponse, next: any) {
|
||||||
|
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
|
||||||
|
let validatedArgs: any[] = [];
|
||||||
|
try {
|
||||||
|
validatedArgs = templateService.getValidatedArgs({ args: argsUsersController_createUser, request, response });
|
||||||
|
|
||||||
|
const controller = new UsersController();
|
||||||
|
|
||||||
|
await templateService.apiHandler({
|
||||||
|
methodName: 'createUser',
|
||||||
|
controller,
|
||||||
|
response,
|
||||||
|
next,
|
||||||
|
validatedArgs,
|
||||||
|
successStatus: 201,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
const argsTasksController_listTasks: Record<string, TsoaRoute.ParameterSchema> = {
|
const argsTasksController_listTasks: Record<string, TsoaRoute.ParameterSchema> = {
|
||||||
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
||||||
orgId: {"in":"query","name":"orgId","dataType":"string"},
|
orgId: {"in":"query","name":"orgId","dataType":"string"},
|
||||||
@@ -468,6 +551,37 @@ export function RegisterRoutes(app: Router) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
const argsProjectsController_createProject: Record<string, TsoaRoute.ParameterSchema> = {
|
||||||
|
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
||||||
|
body: {"in":"body","name":"body","required":true,"ref":"CreateProjectRequest"},
|
||||||
|
};
|
||||||
|
app.post('/projects',
|
||||||
|
...(fetchMiddlewares<RequestHandler>(ProjectsController)),
|
||||||
|
...(fetchMiddlewares<RequestHandler>(ProjectsController.prototype.createProject)),
|
||||||
|
|
||||||
|
async function ProjectsController_createProject(request: ExRequest, response: ExResponse, next: any) {
|
||||||
|
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
|
||||||
|
let validatedArgs: any[] = [];
|
||||||
|
try {
|
||||||
|
validatedArgs = templateService.getValidatedArgs({ args: argsProjectsController_createProject, request, response });
|
||||||
|
|
||||||
|
const controller = new ProjectsController();
|
||||||
|
|
||||||
|
await templateService.apiHandler({
|
||||||
|
methodName: 'createProject',
|
||||||
|
controller,
|
||||||
|
response,
|
||||||
|
next,
|
||||||
|
validatedArgs,
|
||||||
|
successStatus: 201,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
const argsOrganizationsController_listOrganizations: Record<string, TsoaRoute.ParameterSchema> = {
|
const argsOrganizationsController_listOrganizations: Record<string, TsoaRoute.ParameterSchema> = {
|
||||||
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
||||||
};
|
};
|
||||||
@@ -498,6 +612,37 @@ export function RegisterRoutes(app: Router) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
const argsOrganizationsController_createOrganization: Record<string, TsoaRoute.ParameterSchema> = {
|
||||||
|
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
||||||
|
body: {"in":"body","name":"body","required":true,"ref":"CreateOrganizationRequest"},
|
||||||
|
};
|
||||||
|
app.post('/orgs',
|
||||||
|
...(fetchMiddlewares<RequestHandler>(OrganizationsController)),
|
||||||
|
...(fetchMiddlewares<RequestHandler>(OrganizationsController.prototype.createOrganization)),
|
||||||
|
|
||||||
|
async function OrganizationsController_createOrganization(request: ExRequest, response: ExResponse, next: any) {
|
||||||
|
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
|
|
||||||
|
let validatedArgs: any[] = [];
|
||||||
|
try {
|
||||||
|
validatedArgs = templateService.getValidatedArgs({ args: argsOrganizationsController_createOrganization, request, response });
|
||||||
|
|
||||||
|
const controller = new OrganizationsController();
|
||||||
|
|
||||||
|
await templateService.apiHandler({
|
||||||
|
methodName: 'createOrganization',
|
||||||
|
controller,
|
||||||
|
response,
|
||||||
|
next,
|
||||||
|
validatedArgs,
|
||||||
|
successStatus: 201,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||||
const argsOrganizationsController_getOrganization: Record<string, TsoaRoute.ParameterSchema> = {
|
const argsOrganizationsController_getOrganization: Record<string, TsoaRoute.ParameterSchema> = {
|
||||||
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
request: {"in":"request","name":"request","required":true,"dataType":"object"},
|
||||||
orgId: {"in":"path","name":"orgId","required":true,"dataType":"string"},
|
orgId: {"in":"path","name":"orgId","required":true,"dataType":"string"},
|
||||||
|
|||||||
@@ -54,6 +54,67 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
|
"UserEnvelope": {
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"$ref": "#/components/schemas/User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"Record_string.unknown_": {
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": {},
|
||||||
|
"type": "object",
|
||||||
|
"description": "Construct a type with a set of properties K of type T"
|
||||||
|
},
|
||||||
|
"ErrorResponse": {
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"$ref": "#/components/schemas/Record_string.unknown_"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"CreateUserRequest": {
|
||||||
|
"properties": {
|
||||||
|
"orgId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fullName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"orgId",
|
||||||
|
"fullName",
|
||||||
|
"email",
|
||||||
|
"role",
|
||||||
|
"timezone"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"TaskListItem": {
|
"TaskListItem": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -289,27 +350,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
"Record_string.unknown_": {
|
|
||||||
"properties": {},
|
|
||||||
"additionalProperties": {},
|
|
||||||
"type": "object",
|
|
||||||
"description": "Construct a type with a set of properties K of type T"
|
|
||||||
},
|
|
||||||
"ErrorResponse": {
|
|
||||||
"properties": {
|
|
||||||
"error": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"details": {
|
|
||||||
"$ref": "#/components/schemas/Record_string.unknown_"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"error"
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
},
|
|
||||||
"CreateTaskRequest": {
|
"CreateTaskRequest": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"orgId": {
|
"orgId": {
|
||||||
@@ -445,6 +485,50 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
|
"ProjectEnvelope": {
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"$ref": "#/components/schemas/Project"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"project"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"CreateProjectRequest": {
|
||||||
|
"properties": {
|
||||||
|
"orgId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ownerUserId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"dueDate": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"orgId",
|
||||||
|
"key",
|
||||||
|
"name",
|
||||||
|
"status",
|
||||||
|
"ownerUserId"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"OrganizationSummaryListItem": {
|
"OrganizationSummaryListItem": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -595,6 +679,30 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
|
"CreateOrganizationRequest": {
|
||||||
|
"properties": {
|
||||||
|
"slug": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"plan": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"industry": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"slug",
|
||||||
|
"name",
|
||||||
|
"plan",
|
||||||
|
"industry"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"HealthResponse": {
|
"HealthResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
@@ -648,6 +756,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "CreateUser",
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserEnvelope"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid payload",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"security": [],
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateUserRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/tasks": {
|
"/tasks": {
|
||||||
@@ -872,6 +1020,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "CreateProject",
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ProjectEnvelope"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid payload",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Projects"
|
||||||
|
],
|
||||||
|
"security": [],
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateProjectRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/orgs": {
|
"/orgs": {
|
||||||
@@ -894,6 +1082,46 @@
|
|||||||
],
|
],
|
||||||
"security": [],
|
"security": [],
|
||||||
"parameters": []
|
"parameters": []
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "CreateOrganization",
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/OrganizationEnvelope"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid payload",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Organizations"
|
||||||
|
],
|
||||||
|
"security": [],
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateOrganizationRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/orgs/{orgId}": {
|
"/orgs/{orgId}": {
|
||||||
|
|||||||
@@ -99,10 +99,18 @@ export interface UsersEnvelope {
|
|||||||
users: User[];
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserEnvelope {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectsEnvelope {
|
export interface ProjectsEnvelope {
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectEnvelope {
|
||||||
|
project: Project;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TasksEnvelope {
|
export interface TasksEnvelope {
|
||||||
tasks: TaskListItem[];
|
tasks: TaskListItem[];
|
||||||
}
|
}
|
||||||
@@ -123,6 +131,30 @@ export interface CreateTaskRequest {
|
|||||||
dueDate?: string | null;
|
dueDate?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateOrganizationRequest {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
plan: string;
|
||||||
|
industry: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserRequest {
|
||||||
|
orgId: string;
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProjectRequest {
|
||||||
|
orgId: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
ownerUserId: string;
|
||||||
|
dueDate?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateTaskRequest {
|
export interface UpdateTaskRequest {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@@ -28,6 +28,44 @@ function toCamelCaseTask(row: Record<string, unknown>) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUser(db: Database, userId: string) {
|
||||||
|
return db
|
||||||
|
.query(`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
org_id AS orgId,
|
||||||
|
full_name AS fullName,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
timezone
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
`)
|
||||||
|
.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProject(db: Database, projectId: string) {
|
||||||
|
return db
|
||||||
|
.query(`
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.org_id AS orgId,
|
||||||
|
p.key,
|
||||||
|
p.name,
|
||||||
|
p.status,
|
||||||
|
p.owner_user_id AS ownerUserId,
|
||||||
|
p.due_date AS dueDate,
|
||||||
|
u.full_name AS ownerName,
|
||||||
|
COUNT(t.id) AS taskCount
|
||||||
|
FROM projects p
|
||||||
|
LEFT JOIN users u ON u.id = p.owner_user_id
|
||||||
|
LEFT JOIN tasks t ON t.project_id = p.id
|
||||||
|
WHERE p.id = ?
|
||||||
|
GROUP BY p.id
|
||||||
|
`)
|
||||||
|
.get(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
export function listOrganizations(db: Database) {
|
export function listOrganizations(db: Database) {
|
||||||
return db
|
return db
|
||||||
.query(`
|
.query(`
|
||||||
@@ -92,6 +130,25 @@ export function getOrganizationSummary(db: Database, orgId: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateOrganizationInput {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
plan: string;
|
||||||
|
industry: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOrganization(db: Database, input: CreateOrganizationInput) {
|
||||||
|
const id = `org_${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
db.query(`
|
||||||
|
INSERT INTO organizations (id, slug, name, plan, industry, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(id, input.slug, input.name, input.plan, input.industry, now);
|
||||||
|
|
||||||
|
return getOrganizationSummary(db, id);
|
||||||
|
}
|
||||||
|
|
||||||
export function listUsers(db: Database, orgId?: string | null) {
|
export function listUsers(db: Database, orgId?: string | null) {
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
return db
|
return db
|
||||||
@@ -125,6 +182,24 @@ export function listUsers(db: Database, orgId?: string | null) {
|
|||||||
.all();
|
.all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateUserInput {
|
||||||
|
orgId: string;
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUser(db: Database, input: CreateUserInput) {
|
||||||
|
const id = `user_${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
db.query(`
|
||||||
|
INSERT INTO users (id, org_id, full_name, email, role, timezone)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(id, input.orgId, input.fullName, input.email, input.role, input.timezone);
|
||||||
|
|
||||||
|
return getUser(db, id);
|
||||||
|
}
|
||||||
|
|
||||||
export function listProjects(db: Database, orgId?: string | null) {
|
export function listProjects(db: Database, orgId?: string | null) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -148,6 +223,33 @@ export function listProjects(db: Database, orgId?: string | null) {
|
|||||||
return orgId ? db.query(sql).all(orgId) : db.query(sql).all();
|
return orgId ? db.query(sql).all(orgId) : db.query(sql).all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateProjectInput {
|
||||||
|
orgId: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
ownerUserId: string;
|
||||||
|
dueDate?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProject(db: Database, input: CreateProjectInput) {
|
||||||
|
const id = `proj_${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
db.query(`
|
||||||
|
INSERT INTO projects (id, org_id, key, name, status, owner_user_id, due_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
id,
|
||||||
|
input.orgId,
|
||||||
|
input.key,
|
||||||
|
input.name,
|
||||||
|
input.status,
|
||||||
|
input.ownerUserId,
|
||||||
|
input.dueDate ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return getProject(db, id);
|
||||||
|
}
|
||||||
|
|
||||||
export function listTasks(db: Database, filters: TaskFilters = {}) {
|
export function listTasks(db: Database, filters: TaskFilters = {}) {
|
||||||
const conditions: string[] = [];
|
const conditions: string[] = [];
|
||||||
const values: string[] = [];
|
const values: string[] = [];
|
||||||
|
|||||||
@@ -83,6 +83,76 @@ describe("mock task API", () => {
|
|||||||
expect(response.body.organizations).toHaveLength(3);
|
expect(response.body.organizations).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates an organization through the HTTP contract", async () => {
|
||||||
|
const app = createTestApp();
|
||||||
|
|
||||||
|
const response = await sendRequest(app, {
|
||||||
|
method: "POST",
|
||||||
|
url: "/orgs",
|
||||||
|
body: {
|
||||||
|
slug: "globex-systems",
|
||||||
|
name: "Globex Systems",
|
||||||
|
plan: "growth",
|
||||||
|
industry: "Manufacturing",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
expect(response.body.organization.slug).toBe("globex-systems");
|
||||||
|
expect(response.body.organization.name).toBe("Globex Systems");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a user through the HTTP contract", async () => {
|
||||||
|
const app = createTestApp();
|
||||||
|
|
||||||
|
const response = await sendRequest(app, {
|
||||||
|
method: "POST",
|
||||||
|
url: "/users",
|
||||||
|
body: {
|
||||||
|
orgId: "org_acme",
|
||||||
|
fullName: "Iris Quinn",
|
||||||
|
email: "iris@acme.example",
|
||||||
|
role: "engineer",
|
||||||
|
timezone: "Europe/Dublin",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
expect(response.body.user.orgId).toBe("org_acme");
|
||||||
|
expect(response.body.user.email).toBe("iris@acme.example");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a project through the HTTP contract", async () => {
|
||||||
|
const app = createTestApp();
|
||||||
|
|
||||||
|
const response = await sendRequest(app, {
|
||||||
|
method: "POST",
|
||||||
|
url: "/projects",
|
||||||
|
body: {
|
||||||
|
orgId: "org_acme",
|
||||||
|
key: "BIZ",
|
||||||
|
name: "Business Ops Dashboard",
|
||||||
|
status: "planning",
|
||||||
|
ownerUserId: "user_acme_1",
|
||||||
|
dueDate: "2026-06-30",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
expect(response.body.project.orgId).toBe("org_acme");
|
||||||
|
expect(response.body.project.key).toBe("BIZ");
|
||||||
|
expect(response.body.project.ownerUserId).toBe("user_acme_1");
|
||||||
|
});
|
||||||
|
|
||||||
it("filters tasks by org and status", async () => {
|
it("filters tasks by org and status", async () => {
|
||||||
const app = createTestApp();
|
const app = createTestApp();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,17 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { reseedDatabase } from "../src/database";
|
import { reseedDatabase } from "../src/database";
|
||||||
import { createTask, getOrganizationSummary, getTaskDetail, listOrganizations, listTasks, updateTask } from "../src/repository";
|
import {
|
||||||
|
createOrganization,
|
||||||
|
createProject,
|
||||||
|
createTask,
|
||||||
|
createUser,
|
||||||
|
getOrganizationSummary,
|
||||||
|
getTaskDetail,
|
||||||
|
listOrganizations,
|
||||||
|
listTasks,
|
||||||
|
updateTask,
|
||||||
|
} from "../src/repository";
|
||||||
|
|
||||||
let tempDir: string | null = null;
|
let tempDir: string | null = null;
|
||||||
|
|
||||||
@@ -62,6 +72,36 @@ describe("mock task repository", () => {
|
|||||||
expect(updated?.assigneeUserId).toBeNull();
|
expect(updated?.assigneeUserId).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates organization, user, and project records", () => {
|
||||||
|
const db = createDb();
|
||||||
|
|
||||||
|
const organization = createOrganization(db, {
|
||||||
|
slug: "zenith-labs",
|
||||||
|
name: "Zenith Labs",
|
||||||
|
plan: "starter",
|
||||||
|
industry: "Biotech",
|
||||||
|
});
|
||||||
|
const user = createUser(db, {
|
||||||
|
orgId: organization!.id as string,
|
||||||
|
fullName: "Nora Finch",
|
||||||
|
email: "nora@zenith.example",
|
||||||
|
role: "org_admin",
|
||||||
|
timezone: "Europe/Dublin",
|
||||||
|
});
|
||||||
|
const project = createProject(db, {
|
||||||
|
orgId: organization!.id as string,
|
||||||
|
key: "RND",
|
||||||
|
name: "Research Dashboard",
|
||||||
|
status: "planning",
|
||||||
|
ownerUserId: user!.id as string,
|
||||||
|
dueDate: "2026-09-01",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(organization?.slug).toBe("zenith-labs");
|
||||||
|
expect(user?.orgId).toBe(organization?.id);
|
||||||
|
expect(project?.ownerUserId).toBe(user?.id);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns org summary status counts", () => {
|
it("returns org summary status counts", () => {
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
|
|
||||||
|
|||||||
@@ -60,12 +60,15 @@ describe("tsoa swagger document", () => {
|
|||||||
|
|
||||||
expect(operations.sort()).toEqual([
|
expect(operations.sort()).toEqual([
|
||||||
"GET /health",
|
"GET /health",
|
||||||
|
"POST /orgs",
|
||||||
"GET /orgs",
|
"GET /orgs",
|
||||||
"GET /orgs/{orgId}",
|
"GET /orgs/{orgId}",
|
||||||
"GET /orgs/{orgId}/projects",
|
"GET /orgs/{orgId}/projects",
|
||||||
"GET /orgs/{orgId}/tasks",
|
"GET /orgs/{orgId}/tasks",
|
||||||
"GET /users",
|
"GET /users",
|
||||||
|
"POST /users",
|
||||||
"GET /projects",
|
"GET /projects",
|
||||||
|
"POST /projects",
|
||||||
"GET /tasks",
|
"GET /tasks",
|
||||||
"POST /tasks",
|
"POST /tasks",
|
||||||
"GET /tasks/{taskId}",
|
"GET /tasks/{taskId}",
|
||||||
@@ -84,5 +87,8 @@ describe("tsoa swagger document", () => {
|
|||||||
|
|
||||||
expect(schemas.CreateTaskRequest).toBeDefined();
|
expect(schemas.CreateTaskRequest).toBeDefined();
|
||||||
expect(schemas.UpdateTaskRequest).toBeDefined();
|
expect(schemas.UpdateTaskRequest).toBeDefined();
|
||||||
|
expect(schemas.CreateOrganizationRequest).toBeDefined();
|
||||||
|
expect(schemas.CreateUserRequest).toBeDefined();
|
||||||
|
expect(schemas.CreateProjectRequest).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user