From 65ea01f14542f7727179022c376358a922540cda Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Wed, 27 Nov 2024 21:02:34 +0100 Subject: [PATCH 1/9] Add write methods to domain and api and implement project and user insert --- public/swagger.json | 522 +++++++++++++++++++++++- src/controllers/private-rest.ts | 128 +++++- src/db/BadgeHubDataPostgresAdapter.ts | 116 +++++- src/domain/BadgeHubDataPort.ts | 34 ++ src/generated/routes.ts | 567 +++++++++++++++++++++++++- src/util/objectEntries.ts | 18 + 6 files changed, 1362 insertions(+), 23 deletions(-) create mode 100644 src/util/objectEntries.ts diff --git a/public/swagger.json b/public/swagger.json index a9f4a8f..c4639b5 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -574,6 +574,221 @@ "properties": {}, "type": "object", "additionalProperties": false + }, + "Pick_DBInsertUser.Exclude_keyofDBInsertUser.id__": { + "properties": { + "email": { + "type": "string" + }, + "admin": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "remember_token": { + "type": "string" + }, + "editor": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "show_projects": { + "type": "boolean" + }, + "email_verified_at": { + "type": "string" + } + }, + "required": ["email", "name", "password"], + "type": "object", + "description": "From T, pick a set of properties whose keys are in the union K" + }, + "UserProps": { + "properties": { + "email": { + "type": "string" + }, + "admin": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "remember_token": { + "type": "string" + }, + "editor": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "show_projects": { + "type": "boolean" + }, + "email_verified_at": { + "type": "string" + } + }, + "required": ["email", "name", "password"], + "type": "object", + "additionalProperties": false + }, + "Pick_DBInsertProject.Exclude_keyofDBInsertProject.slug__": { + "properties": { + "version_id": { + "type": "number", + "format": "double" + }, + "git": { + "type": "string" + }, + "allow_team_fixes": { + "type": "boolean" + }, + "user_id": { + "type": "string" + } + }, + "required": ["user_id"], + "type": "object", + "description": "From T, pick a set of properties whose keys are in the union K" + }, + "ProjectProps": { + "properties": { + "version_id": { + "type": "number", + "format": "double" + }, + "git": { + "type": "string" + }, + "allow_team_fixes": { + "type": "boolean" + }, + "user_id": { + "type": "string" + } + }, + "required": ["user_id"], + "type": "object", + "additionalProperties": false + }, + "ProjectPropsPartial": { + "properties": { + "version_id": { + "type": "number", + "format": "double" + }, + "git": { + "type": "string" + }, + "allow_team_fixes": { + "type": "boolean" + }, + "user_id": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + }, + "Record_string.string_": { + "properties": {}, + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Construct a type with a set of properties K of type T" + }, + "Record_string._source-string--destination-string_-Array_": { + "properties": {}, + "additionalProperties": { + "items": { + "properties": { + "destination": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": ["destination", "source"], + "type": "object" + }, + "type": "array" + }, + "type": "object", + "description": "Construct a type with a set of properties K of type T" + }, + "DbInsertAppMetadataJSONPartial": { + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "category": { + "$ref": "#/components/schemas/AppCategoryName" + }, + "author": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "license_file": { + "type": "string" + }, + "is_library": { + "type": "boolean" + }, + "is_hidden": { + "type": "boolean" + }, + "semantic_version": { + "type": "string" + }, + "interpreter": { + "type": "string" + }, + "main_executable": { + "type": "string" + }, + "main_executable_overrides": { + "$ref": "#/components/schemas/Record_string.string_" + }, + "file_mappings": { + "items": { + "properties": { + "destination": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": ["destination", "source"], + "type": "object" + }, + "type": "array" + }, + "file_mappings_overrides": { + "$ref": "#/components/schemas/Record_string._source-string--destination-string_-Array_" + } + }, + "type": "object", + "additionalProperties": false } }, "securitySchemes": {} @@ -739,6 +954,89 @@ } } ] + }, + "post": { + "operationId": "CreateApp", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "Create a new app", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "$ref": "#/components/schemas/ProjectSlug" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectProps" + } + } + } + } + }, + "delete": { + "operationId": "DeleteApp", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "Create a new app", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "$ref": "#/components/schemas/ProjectSlug" + } + } + ] + }, + "patch": { + "operationId": "UpdateApp", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "Create a new app", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "$ref": "#/components/schemas/ProjectSlug" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectPropsPartial" + } + } + } + } } }, "/api/v3/apps/{slug}/files/latest/{filePath}": { @@ -895,26 +1193,234 @@ ] } }, - "/api/v3/apps/{slug}/version": { + "/api/v3/users/{userId}": { "post": { - "operationId": "CreateVersion", + "operationId": "InsertUser", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "Create a new user", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProps" + } + } + } + } + } + }, + "/api/v3/apps/{slug}/files/draft/{filePath}": { + "post": { + "operationId": "WriteFile", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "Upload a file to the latest draft version of the project.", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "filePath", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Uint8Array" + } + ] + } + } + } + } + }, + "get": { + "operationId": "GetDraftFile", "responses": { "200": { "description": "Ok", "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/Badge" - }, - "type": "array" + "$ref": "#/components/schemas/Uint8Array" } } } } }, - "description": "Get list of devices (badges)", - "tags": ["public"], + "description": "get the latest draft version of the project.", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "filePath", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/v3/apps/{slug}/metadata/draft": { + "patch": { + "operationId": "ChangeAppMetadata", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "Change the metadata of the latest draft version of the project.", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DbInsertAppMetadataJSONPartial" + } + } + } + } + } + }, + "/api/v3/apps/{slug}/zip/draft": { + "get": { + "operationId": "GetLatestPublishedZip", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Uint8Array" + } + } + } + } + }, + "description": "get the latest draft version of the app in zip format", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "WriteZip", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Version" + } + } + } + } + }, + "description": "Upload a file to the latest draft version of the project.", + "tags": ["private"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Uint8Array" + } + } + } + } + } + }, + "/api/v3/apps/{slug}/publish": { + "patch": { + "operationId": "PublishVersion", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "Publish the latest draft version", + "tags": ["private"], "security": [], "parameters": [ { diff --git a/src/controllers/private-rest.ts b/src/controllers/private-rest.ts index 752eb08..4a800c8 100644 --- a/src/controllers/private-rest.ts +++ b/src/controllers/private-rest.ts @@ -1,14 +1,128 @@ -import { Path, Post, Route, Tags } from "tsoa"; -import { Badge } from "@domain/readModels/Badge"; +import { Body, Delete, Get, Patch, Path, Post, Route, Tags } from "tsoa"; +import type { BadgeHubDataPort } from "@domain/BadgeHubDataPort"; +import { BadgeHubDataPostgresAdapter } from "@db/BadgeHubDataPostgresAdapter"; +import type { Version } from "@domain/readModels/app/Version"; +import type { ProjectSlug } from "@domain/readModels/app/Project"; +import type { DBInsertUser, DBUser } from "@db/models/app/DBUser"; +import type { DBInsertProject } from "@db/models/app/DBProject"; +import type { DBInsertAppMetadataJSON } from "@db/models/app/DBAppMetadataJSON"; +interface UserProps extends Omit {} + +interface ProjectProps extends Omit {} +interface ProjectPropsPartial extends Partial {} +interface DbInsertAppMetadataJSONPartial + extends Partial {} + +// TODO verify user_name against logged in user @Route("/api/v3") -@Tags("public") +@Tags("private") export class PrivateRestController { + public constructor( + private badgeHubData: BadgeHubDataPort = new BadgeHubDataPostgresAdapter() + ) {} + + /** + * Create a new user + */ + @Post("/users/{userId}") + public async insertUser( + @Path() userId: DBUser["id"], + @Body() props: UserProps + ): Promise { + await this.badgeHubData.insertUser({ ...props, id: userId }); + } + + /** + * Create a new app + */ + @Post("/apps/{slug}") + public async createApp( + @Path() slug: ProjectSlug, + @Body() props: ProjectProps + ): Promise { + await this.badgeHubData.insertProject({ ...props, slug }); + } + + /** + * Create a new app + */ + @Delete("/apps/{slug}") + public async deleteApp(@Path() slug: ProjectSlug): Promise { + await this.badgeHubData.deleteProject(slug); + } + + /** + * Create a new app + */ + @Patch("/apps/{slug}") + public async updateApp( + @Path() slug: ProjectSlug, + @Body() changes: ProjectPropsPartial + ): Promise { + await this.badgeHubData.updateProject(slug, changes); + } + + /** + * Upload a file to the latest draft version of the project. + */ + @Post("/apps/{slug}/files/draft/{filePath}") + public async writeFile( + @Path() slug: string, + @Path() filePath: string, + @Body() fileContent: string | Uint8Array + ): Promise { + await this.badgeHubData.writeFile(slug, filePath, fileContent); + } + + /** + * Change the metadata of the latest draft version of the project. + */ + @Patch("/apps/{slug}/metadata/draft") + public async changeAppMetadata( + @Path() slug: string, + @Body() appMetadataChanges: DbInsertAppMetadataJSONPartial + ): Promise { + await this.badgeHubData.updateDraftMetadata(slug, appMetadataChanges); + } + + /** + * get the latest draft version of the project. + */ + @Get("/apps/{slug}/files/draft/{filePath}") + public async getDraftFile( + @Path() slug: string, + @Path() filePath: string + ): Promise { + return await this.badgeHubData.getFileContents(slug, "draft", filePath); + } + + /** + * get the latest draft version of the app in zip format + */ + @Get("/apps/{slug}/zip/draft") + public async getLatestPublishedZip( + @Path() slug: string + ): Promise { + return await this.badgeHubData.getVersionZipContents(slug, "draft"); + } + + /** + * Upload a file to the latest draft version of the project. + */ + @Post("/apps/{slug}/zip/draft") + public async writeZip( + @Path() slug: string, + @Body() zipContent: Uint8Array + ): Promise { + return await this.badgeHubData.writeProjectZip(slug, zipContent); + } + /** - * Get list of devices (badges) + * Publish the latest draft version */ - @Post("/apps/{slug}/version") - public async createVersion(@Path() slug: string): Promise { - throw new Error("Not implemented"); + @Patch("/apps/{slug}/publish") + public async publishVersion(@Path() slug: string) { + await this.badgeHubData.publishVersion(slug); } } diff --git a/src/db/BadgeHubDataPostgresAdapter.ts b/src/db/BadgeHubDataPostgresAdapter.ts index 21bf21b..d799ae0 100644 --- a/src/db/BadgeHubDataPostgresAdapter.ts +++ b/src/db/BadgeHubDataPostgresAdapter.ts @@ -1,12 +1,19 @@ import { BadgeHubDataPort } from "@domain/BadgeHubDataPort"; import { Badge } from "@domain/readModels/Badge"; -import { Project } from "@domain/readModels/app/Project"; +import { + Project, + ProjectCore, + ProjectSlug, +} from "@domain/readModels/app/Project"; import { User } from "@domain/readModels/app/User"; import { Version } from "@domain/readModels/app/Version"; import { Category } from "@domain/readModels/app/Category"; import { Pool } from "pg"; import { getPool } from "@db/connectionPool"; -import sql, { join } from "sql-template-tag"; +import { DBProject as DBProject } from "@db/models/app/DBProject"; +import sql, { join, raw } from "sql-template-tag"; +import { DBInsertUser } from "@db/models/app/DBUser"; +import { Entry, getEntriesWithDefinedValues } from "@util/objectEntries"; import { DBBadge } from "@db/models/DBBadge"; import { getBaseSelectProjectQuery, @@ -21,9 +28,34 @@ import { timestampTZToDate, } from "@db/sqlHelpers/dbDates"; import { DBVersion } from "@db/models/app/DBVersion"; -import { DBAppMetadataJSON } from "@db/models/app/DBAppMetadataJSON"; +import { + DBAppMetadataJSON, + DBInsertAppMetadataJSON, +} from "@db/models/app/DBAppMetadataJSON"; import { DBCategory } from "@db/models/app/DBCategory"; +function getInsertKeysAndValuesSql(user: Object) { + const definedEntries = getEntriesWithDefinedValues(user); + const keys = join(definedEntries.map(([key]) => raw(key))); // raw is ok here because these keys are checked against our typescript definitions by tsoa + const values = join(definedEntries.map(([, value]) => value)); + return { keys, values }; +} + +function getUpdateAssigmentsSql( + definedEntries: Entry>>[] +) { + return join( + definedEntries.map( + ([ + key, + value, + ]) => sql`${raw(key)} // raw is ok here because these keys are checked against our typescript definitions by tsoa + = + ${value}` + ) + ); +} + export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { private readonly pool: Pool; @@ -31,6 +63,13 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { this.pool = getPool(); } + async insertUser(user: DBInsertUser): Promise { + const { keys, values } = getInsertKeysAndValuesSql(user); + const insertQuery = sql`insert into users (${keys}) + values (${values})`; + await this.pool.query(insertQuery); + } + async getCategories(): Promise { const dbCategoryNames: OmitDatedData[] = await this.pool .query(sql`select name, slug from categories`) @@ -38,11 +77,76 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { return dbCategoryNames.map((dbCategory) => dbCategory); } + async insertProject(project: DBProject): Promise { + const { keys, values } = getInsertKeysAndValuesSql(project); + const insertAppMetadataSql = sql`insert into app_metadata_jsons (name) + values (${project.slug})`; + + await this.pool.query(sql` + with inserted_app_metadata as (${insertAppMetadataSql} returning id), + inserted_version as ( + insert + into versions (project_slug, app_metadata_json_id) + values (${project.slug}, (select id from inserted_app_metadata)) returning id) + insert + into projects (${keys}, version_id) + values (${values}, (select id from inserted_version))`); + } + + async updateProject( + projectSlug: ProjectSlug, + changes: Partial> + ): Promise { + const definedEntries = getEntriesWithDefinedValues(changes); + const setters = getUpdateAssigmentsSql(definedEntries); + if (definedEntries.length !== 0) { + await this.pool.query(sql`update projects + set ${setters} + where slug = ${projectSlug}`); + } + } + + async deleteProject(projectSlug: ProjectSlug): Promise { + await this.pool.query(sql`update projects + set deleted_at = now() + where slug = ${projectSlug}`); + } + + writeFile( + projectSlug: ProjectSlug, + filePath: string, + contents: string | Uint8Array + ): Promise { + throw new Error("Method not implemented."); + } + + updateDraftMetadata( + slug: string, + appMetadataChanges: Partial + ): Promise { + throw new Error("Method not implemented."); + } + + writeProjectZip( + projectSlug: string, + zipContent: Uint8Array + ): Promise { + throw new Error("Method not implemented."); + } + + async publishVersion(projectSlug: string): Promise { + await this.pool.query( + sql`update versions v + set published_at=now() + where (published_at is null and v.id = (select version_id from projects p where slug = ${projectSlug}))` + ); + } + async getProject(projectSlug: string): Promise { return (await this.getProjects({ projectSlug }))[0]!; } - // TODO test + // TODO use and test async getDraftVersion(projectSlug: string): Promise { const selectVersionIdForProject = sql`select version_id from projects p where p.slug = ${projectSlug}`; const dbVersion: DBVersion & { app_metadata: DBAppMetadataJSON } = @@ -82,6 +186,10 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { throw new Error("Method not implemented."); } + updateUser(updatedUser: User): Promise { + throw new Error("Method not implemented."); + } + getFileContents( projectSlug: string, versionRevision: number | "draft" | "latest", diff --git a/src/domain/BadgeHubDataPort.ts b/src/domain/BadgeHubDataPort.ts index 5884109..951a382 100644 --- a/src/domain/BadgeHubDataPort.ts +++ b/src/domain/BadgeHubDataPort.ts @@ -4,14 +4,43 @@ import { User } from "@domain/readModels/app/User"; import { FileMetadata } from "@domain/readModels/app/FileMetadata"; import { Badge } from "@domain/readModels/Badge"; import { Category } from "@domain/readModels/app/Category"; +import { DBInsertUser } from "@db/models/app/DBUser"; +import { DBInsertProject, DBProject } from "@db/models/app/DBProject"; +import { DBInsertAppMetadataJSON } from "@db/models/app/DBAppMetadataJSON"; export interface BadgeHubDataPort { + insertUser(user: DBInsertUser): Promise; + + insertProject(project: DBInsertProject): Promise; + + updateProject( + projectSlug: ProjectSlug, + changes: Partial> + ): Promise; + + deleteProject(projectSlug: ProjectSlug): Promise; + + writeFile( + projectSlug: ProjectSlug, + filePath: string, + contents: string | Uint8Array + ): Promise; + + writeProjectZip( + projectSlug: string, + zipContent: Uint8Array + ): Promise; + + publishVersion(projectSlug: ProjectSlug): Promise; // Publishes the current state of the app as a version + getProject(projectSlug: ProjectSlug): Promise; getDraftVersion(projectSlug: ProjectSlug): Promise; getUser(userId: User["id"]): Promise; + updateUser(updatedUser: User): Promise; + getFileContents( projectSlug: Project["slug"], versionRevision: number | "draft" | "latest", @@ -33,4 +62,9 @@ export interface BadgeHubDataPort { badgeSlug?: Badge["slug"]; categorySlug?: Category["slug"]; }): Promise; + + updateDraftMetadata( + slug: string, + appMetadataChanges: Partial + ): Promise; } diff --git a/src/generated/routes.ts b/src/generated/routes.ts index 9097946..4e56852 100644 --- a/src/generated/routes.ts +++ b/src/generated/routes.ts @@ -323,6 +323,138 @@ const models: TsoaRoute.Models = { additionalProperties: false, }, // 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 + "Pick_DBInsertUser.Exclude_keyofDBInsertUser.id__": { + dataType: "refAlias", + type: { + dataType: "nestedObjectLiteral", + nestedProperties: { + email: { dataType: "string", required: true }, + admin: { dataType: "boolean" }, + name: { dataType: "string", required: true }, + password: { dataType: "string", required: true }, + remember_token: { dataType: "string" }, + editor: { dataType: "string" }, + public: { dataType: "boolean" }, + show_projects: { dataType: "boolean" }, + email_verified_at: { dataType: "string" }, + }, + 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 + UserProps: { + dataType: "refObject", + properties: { + email: { dataType: "string", required: true }, + admin: { dataType: "boolean" }, + name: { dataType: "string", required: true }, + password: { dataType: "string", required: true }, + remember_token: { dataType: "string" }, + editor: { dataType: "string" }, + public: { dataType: "boolean" }, + show_projects: { dataType: "boolean" }, + email_verified_at: { dataType: "string" }, + }, + additionalProperties: false, + }, + // 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 + "Pick_DBInsertProject.Exclude_keyofDBInsertProject.slug__": { + dataType: "refAlias", + type: { + dataType: "nestedObjectLiteral", + nestedProperties: { + version_id: { dataType: "double" }, + git: { dataType: "string" }, + allow_team_fixes: { dataType: "boolean" }, + user_id: { dataType: "string", required: true }, + }, + 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 + ProjectProps: { + dataType: "refObject", + properties: { + version_id: { dataType: "double" }, + git: { dataType: "string" }, + allow_team_fixes: { dataType: "boolean" }, + user_id: { dataType: "string", required: true }, + }, + additionalProperties: false, + }, + // 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 + ProjectPropsPartial: { + dataType: "refObject", + properties: { + version_id: { dataType: "double" }, + git: { dataType: "string" }, + allow_team_fixes: { dataType: "boolean" }, + user_id: { dataType: "string" }, + }, + additionalProperties: false, + }, + // 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.string_": { + dataType: "refAlias", + type: { + dataType: "nestedObjectLiteral", + nestedProperties: {}, + additionalProperties: { dataType: "string" }, + 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 + "Record_string._source-string--destination-string_-Array_": { + dataType: "refAlias", + type: { + dataType: "nestedObjectLiteral", + nestedProperties: {}, + additionalProperties: { + dataType: "array", + array: { + dataType: "nestedObjectLiteral", + nestedProperties: { + destination: { dataType: "string", required: true }, + source: { dataType: "string", required: true }, + }, + }, + }, + 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 + DbInsertAppMetadataJSONPartial: { + dataType: "refObject", + properties: { + name: { dataType: "string" }, + description: { dataType: "string" }, + category: { ref: "AppCategoryName" }, + author: { dataType: "string" }, + icon: { dataType: "string" }, + license_file: { dataType: "string" }, + is_library: { dataType: "boolean" }, + is_hidden: { dataType: "boolean" }, + semantic_version: { dataType: "string" }, + interpreter: { dataType: "string" }, + main_executable: { dataType: "string" }, + main_executable_overrides: { ref: "Record_string.string_" }, + file_mappings: { + dataType: "array", + array: { + dataType: "nestedObjectLiteral", + nestedProperties: { + destination: { dataType: "string", required: true }, + source: { dataType: "string", required: true }, + }, + }, + }, + file_mappings_overrides: { + ref: "Record_string._source-string--destination-string_-Array_", + }, + }, + additionalProperties: false, + }, + // 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 templateService = new ExpressTemplateService(models, { noImplicitAdditionalProperties: "throw-on-extras", @@ -700,13 +832,440 @@ 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 app.post( - "/api/v3/apps/:slug/version", + "/api/v3/users/:userId", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.insertUser + ), + + async function PrivateRestController_insertUser( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + userId: { + in: "path", + name: "userId", + required: true, + dataType: "string", + }, + props: { in: "body", name: "props", required: true, ref: "UserProps" }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "insertUser", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.post( + "/api/v3/apps/:slug", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.createApp + ), + + async function PrivateRestController_createApp( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, ref: "ProjectSlug" }, + props: { + in: "body", + name: "props", + required: true, + ref: "ProjectProps", + }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "createApp", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.delete( + "/api/v3/apps/:slug", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.deleteApp + ), + + async function PrivateRestController_deleteApp( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, ref: "ProjectSlug" }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "deleteApp", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.patch( + "/api/v3/apps/:slug", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.updateApp + ), + + async function PrivateRestController_updateApp( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, ref: "ProjectSlug" }, + changes: { + in: "body", + name: "changes", + required: true, + ref: "ProjectPropsPartial", + }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "updateApp", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.post( + "/api/v3/apps/:slug/files/draft/:filePath", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.writeFile + ), + + async function PrivateRestController_writeFile( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, dataType: "string" }, + filePath: { + in: "path", + name: "filePath", + required: true, + dataType: "string", + }, + fileContent: { + in: "body", + name: "fileContent", + required: true, + dataType: "union", + subSchemas: [{ dataType: "string" }, { ref: "Uint8Array" }], + }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "writeFile", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.patch( + "/api/v3/apps/:slug/metadata/draft", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.changeAppMetadata + ), + + async function PrivateRestController_changeAppMetadata( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, dataType: "string" }, + appMetadataChanges: { + in: "body", + name: "appMetadataChanges", + required: true, + ref: "DbInsertAppMetadataJSONPartial", + }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "changeAppMetadata", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.get( + "/api/v3/apps/:slug/files/draft/:filePath", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.getDraftFile + ), + + async function PrivateRestController_getDraftFile( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, dataType: "string" }, + filePath: { + in: "path", + name: "filePath", + required: true, + dataType: "string", + }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "getDraftFile", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.get( + "/api/v3/apps/:slug/zip/draft", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.getLatestPublishedZip + ), + + async function PrivateRestController_getLatestPublishedZip( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, dataType: "string" }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "getLatestPublishedZip", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.post( + "/api/v3/apps/:slug/zip/draft", + ...fetchMiddlewares(PrivateRestController), + ...fetchMiddlewares( + PrivateRestController.prototype.writeZip + ), + + async function PrivateRestController_writeZip( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + slug: { in: "path", name: "slug", required: true, dataType: "string" }, + zipContent: { + in: "body", + name: "zipContent", + required: true, + ref: "Uint8Array", + }, + }; + + // 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, + request, + response, + }); + + const controller = new PrivateRestController(); + + await templateService.apiHandler({ + methodName: "writeZip", + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } 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 + app.patch( + "/api/v3/apps/:slug/publish", ...fetchMiddlewares(PrivateRestController), ...fetchMiddlewares( - PrivateRestController.prototype.createVersion + PrivateRestController.prototype.publishVersion ), - async function PrivateRestController_createVersion( + async function PrivateRestController_publishVersion( request: ExRequest, response: ExResponse, next: any @@ -728,7 +1287,7 @@ export function RegisterRoutes(app: Router) { const controller = new PrivateRestController(); await templateService.apiHandler({ - methodName: "createVersion", + methodName: "publishVersion", controller, response, next, diff --git a/src/util/objectEntries.ts b/src/util/objectEntries.ts new file mode 100644 index 0000000..4d2e477 --- /dev/null +++ b/src/util/objectEntries.ts @@ -0,0 +1,18 @@ +function entryHasDefinedValue( + entry: [K, V] +): entry is [K, V extends undefined ? never : V] { + return entry[1] !== undefined; +} + +export type Entry = NonNullable< + { + [K in keyof T]: [K, T[K]]; + }[keyof T] +>; + +export function getEntriesWithDefinedValues( + obj: T +): Entry[] { + const typedEntries = Object.entries(obj) as Entry[]; + return typedEntries.filter(entryHasDefinedValue); +} From 20834832de3808a6f4454f4f5206a5ec85c5d48d Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Wed, 27 Nov 2024 21:28:12 +0100 Subject: [PATCH 2/9] Add setupPopulateDBApi in case of development mode --- src/index.ts | 7 ++++++- src/{populateDB.ts => setupPopulateDBApi.ts} | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) rename src/{populateDB.ts => setupPopulateDBApi.ts} (99%) diff --git a/src/index.ts b/src/index.ts index f85bca6..7e2852f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,18 @@ import app from "./app"; import { RegisterRoutes } from "./generated/routes"; import { addTsoaValidationFailureLogging } from "@util/logging"; -import { EXPRESS_PORT } from "@config"; +import { EXPRESS_PORT, NODE_ENV } from "@config"; import { disableWriteWhenNotDev } from "@disableWriteWhenNotDev"; import { runMigrations } from "@db/migrations"; +import setupPopulateDBApi from "./setupPopulateDBApi"; async function startServer() { disableWriteWhenNotDev(app); + if (NODE_ENV === "development") { + setupPopulateDBApi(app); + } + RegisterRoutes(app); addTsoaValidationFailureLogging(app); diff --git a/src/populateDB.ts b/src/setupPopulateDBApi.ts similarity index 99% rename from src/populateDB.ts rename to src/setupPopulateDBApi.ts index 1578f39..7d5a2b1 100644 --- a/src/populateDB.ts +++ b/src/setupPopulateDBApi.ts @@ -10,7 +10,7 @@ import { const CATEGORIES_COUNT = 15; -export default async function populateDB(app: Express) { +export default async function setupPopulateDBApi(app: Express) { const router = Router(); app.use(express.json()); From a6bd8b84e7e24a1d717792151f301016a6367ad8 Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Wed, 27 Nov 2024 21:53:59 +0100 Subject: [PATCH 3/9] Change user id back to number like other id's --- README.md | 8 +++- .../sqls/20241116085102-initialize-up.sql | 13 +++--- mockup-data.sql | 40 ++++++++++++++++++- package.json | 1 + src/db/BadgeHubDataPostgresAdapter.ts | 2 +- src/db/models/app/DBUser.ts | 2 +- src/domain/readModels/app/User.ts | 2 +- 7 files changed, 57 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 355288e..0191a7a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,13 @@ For the mock data, we always want up to date tables, so after you have done the You can do this with the pg_dump command in the postgres container: ```bash -npm run backup +npm run overwrite-mockup-data +``` + +Note: you might have to change the db container name in the script. Eg on mac with podman its with underscores instead of dashes which makes the command: + +```bash +docker exec -it badgehub-api_db_1 /usr/bin/pg_dump --username badgehub --schema badgehub badgehub > mockup-data.sql ``` #### Run the down migration to test it. diff --git a/migrations/sqls/20241116085102-initialize-up.sql b/migrations/sqls/20241116085102-initialize-up.sql index 1ffdabb..346c53e 100644 --- a/migrations/sqls/20241116085102-initialize-up.sql +++ b/migrations/sqls/20241116085102-initialize-up.sql @@ -1,9 +1,12 @@ -- back up with old schema alter schema badgehub rename to badgehub_old; create schema badgehub; -create table badgehub.migrations -( - like badgehub_old.migrations including all + +-- Recfreate badgehub.migrations +CREATE TABLE badgehub.migrations ( + id serial NOT NULL, + name character varying(255) NOT NULL, + run_on timestamp without time zone NOT NULL ); -- create tables @@ -19,7 +22,7 @@ create table badges create table users ( - id text primary key, + id serial primary key, email text unique, admin boolean, name text not null, @@ -40,7 +43,7 @@ create table projects updated_at timestamptz not null default now(), deleted_at timestamptz, version_id integer, - user_id text not null, + user_id integer not null, slug text not null primary key, git text, allow_team_fixes boolean, diff --git a/mockup-data.sql b/mockup-data.sql index 7d183d1..968d047 100644 --- a/mockup-data.sql +++ b/mockup-data.sql @@ -192,7 +192,7 @@ CREATE TABLE badgehub.projects ( updated_at timestamp with time zone DEFAULT now() NOT NULL, deleted_at timestamp with time zone, version_id integer, - user_id text NOT NULL, + user_id integer NOT NULL, slug text NOT NULL, git text, allow_team_fixes boolean @@ -206,7 +206,7 @@ ALTER TABLE badgehub.projects OWNER TO badgehub; -- CREATE TABLE badgehub.users ( - id text NOT NULL, + id integer NOT NULL, email text, admin boolean, name text NOT NULL, @@ -224,6 +224,28 @@ CREATE TABLE badgehub.users ( ALTER TABLE badgehub.users OWNER TO badgehub; +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: badgehub; Owner: badgehub +-- + +CREATE SEQUENCE badgehub.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE badgehub.users_id_seq OWNER TO badgehub; + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: badgehub; Owner: badgehub +-- + +ALTER SEQUENCE badgehub.users_id_seq OWNED BY badgehub.users.id; + + -- -- Name: versioned_dependencies; Type: TABLE; Schema: badgehub; Owner: badgehub -- @@ -329,6 +351,13 @@ ALTER TABLE ONLY badgehub.migrations ALTER COLUMN id SET DEFAULT nextval('badgeh ALTER TABLE ONLY badgehub.project_statuses_on_badges ALTER COLUMN id SET DEFAULT nextval('badgehub.project_statuses_on_badges_id_seq'::regclass); +-- +-- Name: users id; Type: DEFAULT; Schema: badgehub; Owner: badgehub +-- + +ALTER TABLE ONLY badgehub.users ALTER COLUMN id SET DEFAULT nextval('badgehub.users_id_seq'::regclass); + + -- -- Name: versioned_dependencies id; Type: DEFAULT; Schema: badgehub; Owner: badgehub -- @@ -892,6 +921,13 @@ SELECT pg_catalog.setval('badgehub.migrations_id_seq', 1, true); SELECT pg_catalog.setval('badgehub.project_statuses_on_badges_id_seq', 105, true); +-- +-- Name: users_id_seq; Type: SEQUENCE SET; Schema: badgehub; Owner: badgehub +-- + +SELECT pg_catalog.setval('badgehub.users_id_seq', 1, false); + + -- -- Name: versioned_dependencies_id_seq; Type: SEQUENCE SET; Schema: badgehub; Owner: badgehub -- diff --git a/package.json b/package.json index d4c1f53..644e510 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test-db:up": "docker compose -f docker-compose.test-db.yml up -d --wait", "test-db:down": "docker compose -f docker-compose.test-db.yml down", "backup": "docker exec -it badgehub-api-db-1 /usr/bin/pg_dump --username badgehub badgehub -f /var/backup/data-backup-`date +\"%Y-%m-%dT%H:%m\"`.sql", + "overwrite-mockup-data": "docker exec -it badgehub-api-db-1 /usr/bin/pg_dump --username badgehub --schema badgehub badgehub > mockup-data.sql", "test": "vitest --coverage.enabled true", "db-migrate:up": "db-migrate up", "db-migrate:down": "db-migrate down", diff --git a/src/db/BadgeHubDataPostgresAdapter.ts b/src/db/BadgeHubDataPostgresAdapter.ts index d799ae0..9603423 100644 --- a/src/db/BadgeHubDataPostgresAdapter.ts +++ b/src/db/BadgeHubDataPostgresAdapter.ts @@ -182,7 +182,7 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { }; } - getUser(userId: string): Promise { + getUser(userId: User["id"]): Promise { throw new Error("Method not implemented."); } diff --git a/src/db/models/app/DBUser.ts b/src/db/models/app/DBUser.ts index 53c7177..f0bc936 100644 --- a/src/db/models/app/DBUser.ts +++ b/src/db/models/app/DBUser.ts @@ -5,7 +5,7 @@ export interface UserRelation { user_id: DBUser["id"]; } export interface DBInsertUser { - id: string; + id: number; email: string; admin?: boolean; name: string; diff --git a/src/domain/readModels/app/User.ts b/src/domain/readModels/app/User.ts index fb8b0f3..0e6d094 100644 --- a/src/domain/readModels/app/User.ts +++ b/src/domain/readModels/app/User.ts @@ -5,7 +5,7 @@ export interface UserRelation { } export interface User extends DatedData { - id: string; + id: number; email: string; admin: boolean; name: string; From d6793481ed4f887e11b728595c44828a23d74e91 Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Wed, 27 Nov 2024 22:03:37 +0100 Subject: [PATCH 4/9] Extract objectToSQL.ts --- src/db/BadgeHubDataPostgresAdapter.ts | 8 +------- src/db/sqlHelpers/objectToSQL.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 src/db/sqlHelpers/objectToSQL.ts diff --git a/src/db/BadgeHubDataPostgresAdapter.ts b/src/db/BadgeHubDataPostgresAdapter.ts index 9603423..17108fb 100644 --- a/src/db/BadgeHubDataPostgresAdapter.ts +++ b/src/db/BadgeHubDataPostgresAdapter.ts @@ -33,13 +33,7 @@ import { DBInsertAppMetadataJSON, } from "@db/models/app/DBAppMetadataJSON"; import { DBCategory } from "@db/models/app/DBCategory"; - -function getInsertKeysAndValuesSql(user: Object) { - const definedEntries = getEntriesWithDefinedValues(user); - const keys = join(definedEntries.map(([key]) => raw(key))); // raw is ok here because these keys are checked against our typescript definitions by tsoa - const values = join(definedEntries.map(([, value]) => value)); - return { keys, values }; -} +import { getInsertKeysAndValuesSql } from "@db/sqlHelpers/objectToSQL"; function getUpdateAssigmentsSql( definedEntries: Entry>>[] diff --git a/src/db/sqlHelpers/objectToSQL.ts b/src/db/sqlHelpers/objectToSQL.ts new file mode 100644 index 0000000..00e32ea --- /dev/null +++ b/src/db/sqlHelpers/objectToSQL.ts @@ -0,0 +1,9 @@ +import { getEntriesWithDefinedValues } from "@util/objectEntries"; +import { join, raw } from "sql-template-tag"; + +export function getInsertKeysAndValuesSql(user: Object) { + const definedEntries = getEntriesWithDefinedValues(user); + const keys = join(definedEntries.map(([key]) => raw(key))); // raw is ok here because these keys are checked against our typescript definitions by tsoa + const values = join(definedEntries.map(([, value]) => value)); + return { keys, values }; +} From 339dc0a3b1eda9bc302246ab27b782fd5a82238e Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Wed, 27 Nov 2024 23:07:54 +0100 Subject: [PATCH 5/9] Fix populate --- src/db/BadgeHubDataPostgresAdapter.ts | 62 ++++++--- src/db/models/DBProjectStatusOnBadge.ts | 12 +- src/db/models/README.md | 1 + src/db/sqlHelpers/objectToSQL.ts | 13 +- src/generated/routes.ts | 4 +- src/setupPopulateDBApi.ts | 169 +++++++++++++----------- 6 files changed, 160 insertions(+), 101 deletions(-) diff --git a/src/db/BadgeHubDataPostgresAdapter.ts b/src/db/BadgeHubDataPostgresAdapter.ts index 17108fb..2ce587c 100644 --- a/src/db/BadgeHubDataPostgresAdapter.ts +++ b/src/db/BadgeHubDataPostgresAdapter.ts @@ -13,7 +13,7 @@ import { getPool } from "@db/connectionPool"; import { DBProject as DBProject } from "@db/models/app/DBProject"; import sql, { join, raw } from "sql-template-tag"; import { DBInsertUser } from "@db/models/app/DBUser"; -import { Entry, getEntriesWithDefinedValues } from "@util/objectEntries"; +import { getEntriesWithDefinedValues } from "@util/objectEntries"; import { DBBadge } from "@db/models/DBBadge"; import { getBaseSelectProjectQuery, @@ -33,19 +33,19 @@ import { DBInsertAppMetadataJSON, } from "@db/models/app/DBAppMetadataJSON"; import { DBCategory } from "@db/models/app/DBCategory"; -import { getInsertKeysAndValuesSql } from "@db/sqlHelpers/objectToSQL"; +import { + assertValidColumKey, + getInsertKeysAndValuesSql, +} from "@db/sqlHelpers/objectToSQL"; -function getUpdateAssigmentsSql( - definedEntries: Entry>>[] -) { +function getUpdateAssigmentsSql(changes: Object) { + const changeEntries = getEntriesWithDefinedValues(changes); + if (!changeEntries.length) { + return; + } return join( - definedEntries.map( - ([ - key, - value, - ]) => sql`${raw(key)} // raw is ok here because these keys are checked against our typescript definitions by tsoa - = - ${value}` + changeEntries.map( + ([key, value]) => sql`${raw(assertValidColumKey(key))} = ${value}` ) ); } @@ -91,13 +91,13 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { projectSlug: ProjectSlug, changes: Partial> ): Promise { - const definedEntries = getEntriesWithDefinedValues(changes); - const setters = getUpdateAssigmentsSql(definedEntries); - if (definedEntries.length !== 0) { - await this.pool.query(sql`update projects - set ${setters} - where slug = ${projectSlug}`); + const setters = getUpdateAssigmentsSql(changes); + if (!setters) { + return; } + await this.pool.query(sql`update projects + set ${setters} + where slug = ${projectSlug}`); } async deleteProject(projectSlug: ProjectSlug): Promise { @@ -106,12 +106,34 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { where slug = ${projectSlug}`); } - writeFile( + async writeFile( projectSlug: ProjectSlug, filePath: string, contents: string | Uint8Array ): Promise { - throw new Error("Method not implemented."); + if (filePath === "metadata.json") { + const appMetadata: DBAppMetadataJSON = JSON.parse( + typeof contents === "string" + ? contents + : new TextDecoder().decode(contents) + ); + const setters = getUpdateAssigmentsSql(appMetadata); + if (!setters) { + return; + } + + const appMetadataUpdateQuery = sql`update app_metadata_jsons + set ${setters} + where id = (select app_metadata_json_id + from versions v + where v.id = + (select projects.version_id from projects where slug = ${projectSlug}))`; + await this.pool.query(appMetadataUpdateQuery); + } else { + throw new Error( + "Method not implemented for files other than the metadata.json file yet." + ); + } } updateDraftMetadata( diff --git a/src/db/models/DBProjectStatusOnBadge.ts b/src/db/models/DBProjectStatusOnBadge.ts index 7b47772..6d5446e 100644 --- a/src/db/models/DBProjectStatusOnBadge.ts +++ b/src/db/models/DBProjectStatusOnBadge.ts @@ -3,11 +3,15 @@ import { BadgeSlugRelation } from "./DBBadge"; import { ProjectStatusName } from "@domain/readModels/app/Project"; import { ProjectSlugRelation } from "@db/models/app/DBProject"; +export interface DBInsertProjectStatusOnBadge + extends BadgeSlugRelation, + ProjectSlugRelation { + status?: ProjectStatusName; // Status for this project for this particular badge +} + // table name: project_statuses_on_badges export interface DBProjectStatusOnBadge - extends DBDatedData, - BadgeSlugRelation, - ProjectSlugRelation { + extends DBInsertProjectStatusOnBadge, + DBDatedData { id: number; - status: ProjectStatusName; // Status for this project for this particular badge } diff --git a/src/db/models/README.md b/src/db/models/README.md index 451756b..ba84b84 100644 --- a/src/db/models/README.md +++ b/src/db/models/README.md @@ -5,3 +5,4 @@ In the db models, we try to stick to 2 principles - DRY, no data duplication, unless it's accidental duplication - No arrays. Arrays make querying and switching to another DB more difficult. So instead of arrays, we try to work with relation tables. - There is currently an exception here for the MetaDataFileContents, there the mappings are records and arrays and records with arrays, this is because the data comes from the json file and we could also just store it as stringified JSON. +- Prefer slugs as primary keys where possible, because they are more human-readable which makes the api calls and db queries easier to read. diff --git a/src/db/sqlHelpers/objectToSQL.ts b/src/db/sqlHelpers/objectToSQL.ts index 00e32ea..fea19e8 100644 --- a/src/db/sqlHelpers/objectToSQL.ts +++ b/src/db/sqlHelpers/objectToSQL.ts @@ -1,9 +1,20 @@ import { getEntriesWithDefinedValues } from "@util/objectEntries"; import { join, raw } from "sql-template-tag"; +const COLUMN_KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + +export function assertValidColumKey(key: string): string { + if (!COLUMN_KEY_REGEX.test(key)) { + throw new Error("Invalid column key: " + key); + } + return key; +} + export function getInsertKeysAndValuesSql(user: Object) { const definedEntries = getEntriesWithDefinedValues(user); - const keys = join(definedEntries.map(([key]) => raw(key))); // raw is ok here because these keys are checked against our typescript definitions by tsoa + const keys = join( + definedEntries.map(([key]) => raw(assertValidColumKey(key))) + ); const values = join(definedEntries.map(([, value]) => value)); return { keys, values }; } diff --git a/src/generated/routes.ts b/src/generated/routes.ts index 4e56852..25f3fcd 100644 --- a/src/generated/routes.ts +++ b/src/generated/routes.ts @@ -2,15 +2,15 @@ /* eslint-disable */ // 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 import type { TsoaRoute } from "@tsoa/runtime"; -import { fetchMiddlewares, ExpressTemplateService } from "@tsoa/runtime"; +import { ExpressTemplateService, fetchMiddlewares } from "@tsoa/runtime"; // 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 import { PublicRestController } from "./../controllers/public-rest.js"; // 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 import { PrivateRestController } from "./../controllers/private-rest.js"; import type { Request as ExRequest, - Response as ExResponse, RequestHandler, + Response as ExResponse, Router, } from "express"; diff --git a/src/setupPopulateDBApi.ts b/src/setupPopulateDBApi.ts index 7d5a2b1..f6c3409 100644 --- a/src/setupPopulateDBApi.ts +++ b/src/setupPopulateDBApi.ts @@ -7,9 +7,33 @@ import { POSTGRES_PORT, POSTGRES_USER, } from "@config"; +import sql from "sql-template-tag"; +import { DBInsertUser } from "@db/models/app/DBUser"; +import { DBDatedData } from "@db/models/app/DBDatedData"; +import { DBInsertProject } from "@db/models/app/DBProject"; +import { BadgeHubDataPostgresAdapter } from "@db/BadgeHubDataPostgresAdapter"; +import { DBInsertAppMetadataJSON } from "@db/models/app/DBAppMetadataJSON"; +import { getInsertKeysAndValuesSql } from "@db/sqlHelpers/objectToSQL"; +import { DBInsertProjectStatusOnBadge } from "@db/models/DBProjectStatusOnBadge"; -const CATEGORIES_COUNT = 15; - +const CATEGORY_NAMES = [ + "Uncategorised", + "Event related", + "Games", + "Graphics", + "Hardware", + "Utility", + "Wearable", + "Data", + "Silly", + "Hacking", + "Troll", + "Unusable", + "Adult", + "Virus", + // "Interpreter", // TODO add Interpreter to mock data? +] as const; +const CATEGORIES_COUNT = CATEGORY_NAMES.length; export default async function setupPopulateDBApi(app: Express) { const router = Router(); @@ -25,27 +49,29 @@ export default async function setupPopulateDBApi(app: Express) { port: POSTGRES_PORT, }); - router.get("/populate", async (req, res) => { - await deleteDatabases(pool); + router.post("/populate", async (req, res) => { + await cleanDatabases(pool); await populateDatabases(pool); return res.status(200).send("Population done."); }); } -async function deleteDatabases(pool: pg.Pool) { +async function cleanDatabases(pool: pg.Pool) { const client = await pool.connect(); - await client.query("DELETE FROM badgehub.badge_project"); - await client.query("DELETE FROM badgehub.dependencies"); - await client.query("DELETE FROM badgehub.users"); - await client.query("DELETE FROM badgehub.projects"); + await client.query(sql`delete from badgehub.users`); + await client.query(sql`delete from badgehub.projects`); + await client.query(sql`delete from badgehub.app_metadata_jsons`); + await client.query(sql`delete from badgehub.versions`); + await client.query(sql`delete from badgehub.versioned_dependencies`); + await client.query(sql`delete from badgehub.project_statuses_on_badges`); client.release(); } async function populateDatabases(pool: pg.Pool) { const client = await pool.connect(); const userCount = await insertUsers(client); - const projectCount = await insertProjects(client, userCount); - await badgeProjectCrossTable(client, projectCount); + const projectSlugs = await insertProjects(client, userCount); + await badgeProjectCrossTable(client, projectSlugs); client.release(); } @@ -156,7 +182,7 @@ async function insertUsers(client: pg.PoolClient) { "hotmail.com", ]; - for (const id in users) { + for (let id = 0; id < users.length; id++) { const isAdmin = random(10) == 0; const name = users[id]!; const email = `${name.toLowerCase()}@${domains[random(domains.length)]}`; @@ -168,30 +194,26 @@ async function insertUsers(client: pg.PoolClient) { const updatedAt = date(createDate + random(100)); console.log(`insert into users ${name}`); - - await client.query( - `INSERT INTO badgehub.users - (id, admin, name, email, password, public, show_projects, created_at, updated_at) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - [ - id, - isAdmin, - name, - email, - password, - isPublic, - showProjects, - createdAt, - updatedAt, - ] - ); + const toInsert: DBInsertUser & DBDatedData = { + id, + admin: isAdmin, + name, + email, + password, + public: isPublic, + show_projects: showProjects, + created_at: createdAt, + updated_at: updatedAt, + }; + const badgeHubAdapter = new BadgeHubDataPostgresAdapter(); + await badgeHubAdapter.insertUser(toInsert); } return users.length; } async function insertProjects(client: pg.PoolClient, userCount: number) { - const apps = [ + const projectSlugs = [ "CodeCraft", "PixelPulse", "BitBlast", @@ -280,74 +302,73 @@ async function insertProjects(client: pg.PoolClient, userCount: number) { "HackQuest", "SecureSphere", ]; + const badgeHubAdapter = new BadgeHubDataPostgresAdapter(); - for (const id in apps) { - const name = apps[id]!; + for (let id = 0; id < projectSlugs.length; id++) { + const name = projectSlugs[id]!; const slug = name.toLowerCase(); const description = getDescription(name); const userId = random(userCount); - const categoryId = random(CATEGORIES_COUNT) + 1; + const categoryId = random(CATEGORIES_COUNT); const createDate = -random(600); const createdAt = date(createDate); const updatedAt = date(createDate + random(100)); console.log(`insert into projects ${name} (${description})`); - await client.query( - `INSERT INTO badgehub.projects - (id, name, slug, description, user_id, category_id, created_at, updated_at) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8)`, - [id, name, slug, description, userId, categoryId, createdAt, updatedAt] + const inserted: DBInsertProject & DBDatedData = { + slug, + user_id: userId, + created_at: createdAt, + updated_at: updatedAt, + }; + + await badgeHubAdapter.insertProject(inserted); + const appMetadata: DBInsertAppMetadataJSON & DBDatedData = { + name, + description, + category: CATEGORY_NAMES[categoryId], + created_at: createdAt, + updated_at: updatedAt, + }; + + await badgeHubAdapter.writeFile( + inserted.slug, + "metadata.json", + JSON.stringify(appMetadata) ); } - return apps.length; + return projectSlugs.map((slug) => slug.toLowerCase()); } async function badgeProjectCrossTable( client: pg.PoolClient, - projectCount: number + projectSlugs: string[] ) { - const badgeIds = [1, 2, 5]; // Hardcoded! Update by hand - for (let index = 0; index < projectCount; index++) { - const badgeId = badgeIds[random(3)]; + const badgeSlugs = ["mch2022", "troopers23", "why2025"] as const; // Hardcoded! Update by hand + for (let index = 0; index < projectSlugs.length; index++) { + const badgeSlug = badgeSlugs[random(3)]!; + let insertObject1: DBInsertProjectStatusOnBadge = { + badge_slug: badgeSlug, + project_slug: projectSlugs[index]!, + }; + const insert1 = getInsertKeysAndValuesSql(insertObject1); await client.query( - `INSERT INTO badgehub.badge_project - (badge_id, project_id) VALUES - ($1, $2)`, - [badgeId, index] + sql`insert into badgehub.project_statuses_on_badges (${insert1.keys}) values (${insert1.values})` ); // Some project support two badges - const badgeId2 = badgeIds[random(3)]; - if (badgeId2 != badgeId && random(3) == 1) { + const badgeId2 = badgeSlugs[random(3)]!; + if (badgeId2 != badgeSlug && random(3) == 1) { + const insertObject2: DBInsertProjectStatusOnBadge = { + badge_slug: badgeId2, + project_slug: projectSlugs[index]!, + }; + const insert2 = getInsertKeysAndValuesSql(insertObject2); await client.query( - `INSERT INTO badgehub.badge_project - (badge_id, project_id) VALUES - ($1, $2)`, - [badgeId2, index] + sql`insert into badgehub.project_statuses_on_badges (${insert2.keys}) values (${insert2.values})` ); } } } - -// Not in use right now -async function userProjectsCrossTable( - client: pg.PoolClient, - userCount: number, - projectCount: number -) { - for (let index = 0; index < 300; index++) { - const userId = random(userCount); - const projectId = random(projectCount); - const createDate = -random(600); - const createdAt = date(createDate); - const updatedAt = date(createDate + random(100)); - await client.query( - `INSERT INTO badgehub.project_user - (id, user_id, project_id, created_at, updated_at) VALUES - ($1, $2, $3, $4, $5)`, - [index, userId, projectId, createdAt, updatedAt] - ); - } -} From 865da35305bf4732bb6a35ee9ce4ab4e7d80cbb3 Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Thu, 28 Nov 2024 00:18:33 +0100 Subject: [PATCH 6/9] swap location of draft in api url path Co-authored-by: zera1ul <179273071+zera1ul@users.noreply.github.com> --- src/controllers/private-rest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/private-rest.ts b/src/controllers/private-rest.ts index 4a800c8..e25c5d9 100644 --- a/src/controllers/private-rest.ts +++ b/src/controllers/private-rest.ts @@ -66,7 +66,7 @@ export class PrivateRestController { /** * Upload a file to the latest draft version of the project. */ - @Post("/apps/{slug}/files/draft/{filePath}") + @Post("/apps/{slug}/draft/files/{filePath}") public async writeFile( @Path() slug: string, @Path() filePath: string, @@ -78,7 +78,7 @@ export class PrivateRestController { /** * Change the metadata of the latest draft version of the project. */ - @Patch("/apps/{slug}/metadata/draft") + @Patch("/apps/{slug}/draft/metadata") public async changeAppMetadata( @Path() slug: string, @Body() appMetadataChanges: DbInsertAppMetadataJSONPartial @@ -89,7 +89,7 @@ export class PrivateRestController { /** * get the latest draft version of the project. */ - @Get("/apps/{slug}/files/draft/{filePath}") + @Get("/apps/{slug}/draft/files/{filePath}") public async getDraftFile( @Path() slug: string, @Path() filePath: string @@ -100,7 +100,7 @@ export class PrivateRestController { /** * get the latest draft version of the app in zip format */ - @Get("/apps/{slug}/zip/draft") + @Get("/apps/{slug}/draft/zip") public async getLatestPublishedZip( @Path() slug: string ): Promise { @@ -110,7 +110,7 @@ export class PrivateRestController { /** * Upload a file to the latest draft version of the project. */ - @Post("/apps/{slug}/zip/draft") + @Post("/apps/{slug}/draft/zip") public async writeZip( @Path() slug: string, @Body() zipContent: Uint8Array From 23c9f5eadbfde099651cec88d31ecfae523d3670 Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Sat, 30 Nov 2024 20:32:16 +0100 Subject: [PATCH 7/9] update swagger for draft location switch --- public/swagger.json | 24 +++++++++++++++--------- src/generated/routes.ts | 26 +++++++++++++------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/public/swagger.json b/public/swagger.json index c4639b5..41377a2 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -148,7 +148,8 @@ "format": "date-time" }, "id": { - "type": "string" + "type": "number", + "format": "double" }, "email": { "type": "string" @@ -447,7 +448,8 @@ "type": "string" }, "user_id": { - "type": "string" + "type": "number", + "format": "double" }, "git": { "type": "string" @@ -656,7 +658,8 @@ "type": "boolean" }, "user_id": { - "type": "string" + "type": "number", + "format": "double" } }, "required": ["user_id"], @@ -676,7 +679,8 @@ "type": "boolean" }, "user_id": { - "type": "string" + "type": "number", + "format": "double" } }, "required": ["user_id"], @@ -696,7 +700,8 @@ "type": "boolean" }, "user_id": { - "type": "string" + "type": "number", + "format": "double" } }, "type": "object", @@ -1210,7 +1215,8 @@ "name": "userId", "required": true, "schema": { - "type": "string" + "format": "double", + "type": "number" } } ], @@ -1226,7 +1232,7 @@ } } }, - "/api/v3/apps/{slug}/files/draft/{filePath}": { + "/api/v3/apps/{slug}/draft/files/{filePath}": { "post": { "operationId": "WriteFile", "responses": { @@ -1310,7 +1316,7 @@ ] } }, - "/api/v3/apps/{slug}/metadata/draft": { + "/api/v3/apps/{slug}/draft/metadata": { "patch": { "operationId": "ChangeAppMetadata", "responses": { @@ -1343,7 +1349,7 @@ } } }, - "/api/v3/apps/{slug}/zip/draft": { + "/api/v3/apps/{slug}/draft/zip": { "get": { "operationId": "GetLatestPublishedZip", "responses": { diff --git a/src/generated/routes.ts b/src/generated/routes.ts index 25f3fcd..a3d701d 100644 --- a/src/generated/routes.ts +++ b/src/generated/routes.ts @@ -2,15 +2,15 @@ /* eslint-disable */ // 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 import type { TsoaRoute } from "@tsoa/runtime"; -import { ExpressTemplateService, fetchMiddlewares } from "@tsoa/runtime"; +import { fetchMiddlewares, ExpressTemplateService } from "@tsoa/runtime"; // 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 import { PublicRestController } from "./../controllers/public-rest.js"; // 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 import { PrivateRestController } from "./../controllers/private-rest.js"; import type { Request as ExRequest, - RequestHandler, Response as ExResponse, + RequestHandler, Router, } from "express"; @@ -107,7 +107,7 @@ const models: TsoaRoute.Models = { created_at: { dataType: "datetime", required: true }, updated_at: { dataType: "datetime", required: true }, deleted_at: { dataType: "datetime" }, - id: { dataType: "string", required: true }, + id: { dataType: "double", required: true }, email: { dataType: "string", required: true }, admin: { dataType: "boolean", required: true }, name: { dataType: "string", required: true }, @@ -266,7 +266,7 @@ const models: TsoaRoute.Models = { dataType: "refObject", properties: { slug: { dataType: "string", required: true }, - user_id: { dataType: "string", required: true }, + user_id: { dataType: "double", required: true }, git: { dataType: "string" }, allow_team_fixes: { dataType: "boolean" }, created_at: { dataType: "datetime", required: true }, @@ -366,7 +366,7 @@ const models: TsoaRoute.Models = { version_id: { dataType: "double" }, git: { dataType: "string" }, allow_team_fixes: { dataType: "boolean" }, - user_id: { dataType: "string", required: true }, + user_id: { dataType: "double", required: true }, }, validators: {}, }, @@ -378,7 +378,7 @@ const models: TsoaRoute.Models = { version_id: { dataType: "double" }, git: { dataType: "string" }, allow_team_fixes: { dataType: "boolean" }, - user_id: { dataType: "string", required: true }, + user_id: { dataType: "double", required: true }, }, additionalProperties: false, }, @@ -389,7 +389,7 @@ const models: TsoaRoute.Models = { version_id: { dataType: "double" }, git: { dataType: "string" }, allow_team_fixes: { dataType: "boolean" }, - user_id: { dataType: "string" }, + user_id: { dataType: "double" }, }, additionalProperties: false, }, @@ -848,7 +848,7 @@ export function RegisterRoutes(app: Router) { in: "path", name: "userId", required: true, - dataType: "string", + dataType: "double", }, props: { in: "body", name: "props", required: true, ref: "UserProps" }, }; @@ -1018,7 +1018,7 @@ 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 app.post( - "/api/v3/apps/:slug/files/draft/:filePath", + "/api/v3/apps/:slug/draft/files/:filePath", ...fetchMiddlewares(PrivateRestController), ...fetchMiddlewares( PrivateRestController.prototype.writeFile @@ -1073,7 +1073,7 @@ 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 app.patch( - "/api/v3/apps/:slug/metadata/draft", + "/api/v3/apps/:slug/draft/metadata", ...fetchMiddlewares(PrivateRestController), ...fetchMiddlewares( PrivateRestController.prototype.changeAppMetadata @@ -1121,7 +1121,7 @@ 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 app.get( - "/api/v3/apps/:slug/files/draft/:filePath", + "/api/v3/apps/:slug/draft/files/:filePath", ...fetchMiddlewares(PrivateRestController), ...fetchMiddlewares( PrivateRestController.prototype.getDraftFile @@ -1169,7 +1169,7 @@ 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 app.get( - "/api/v3/apps/:slug/zip/draft", + "/api/v3/apps/:slug/draft/zip", ...fetchMiddlewares(PrivateRestController), ...fetchMiddlewares( PrivateRestController.prototype.getLatestPublishedZip @@ -1211,7 +1211,7 @@ 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 app.post( - "/api/v3/apps/:slug/zip/draft", + "/api/v3/apps/:slug/draft/zip", ...fetchMiddlewares(PrivateRestController), ...fetchMiddlewares( PrivateRestController.prototype.writeZip From cda13413105bb4833d668172bcd5a10ec6830ed1 Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Tue, 3 Dec 2024 20:40:10 +0100 Subject: [PATCH 8/9] rename writeFile and Zip to include Draft --- src/controllers/private-rest.ts | 4 ++-- src/db/BadgeHubDataPostgresAdapter.ts | 4 ++-- src/domain/BadgeHubDataPort.ts | 4 ++-- src/setupPopulateDBApi.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/private-rest.ts b/src/controllers/private-rest.ts index e25c5d9..0e68a5c 100644 --- a/src/controllers/private-rest.ts +++ b/src/controllers/private-rest.ts @@ -72,7 +72,7 @@ export class PrivateRestController { @Path() filePath: string, @Body() fileContent: string | Uint8Array ): Promise { - await this.badgeHubData.writeFile(slug, filePath, fileContent); + await this.badgeHubData.writeDraftFile(slug, filePath, fileContent); } /** @@ -115,7 +115,7 @@ export class PrivateRestController { @Path() slug: string, @Body() zipContent: Uint8Array ): Promise { - return await this.badgeHubData.writeProjectZip(slug, zipContent); + return await this.badgeHubData.writeDraftProjectZip(slug, zipContent); } /** diff --git a/src/db/BadgeHubDataPostgresAdapter.ts b/src/db/BadgeHubDataPostgresAdapter.ts index 2ce587c..69feb0d 100644 --- a/src/db/BadgeHubDataPostgresAdapter.ts +++ b/src/db/BadgeHubDataPostgresAdapter.ts @@ -106,7 +106,7 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { where slug = ${projectSlug}`); } - async writeFile( + async writeDraftFile( projectSlug: ProjectSlug, filePath: string, contents: string | Uint8Array @@ -143,7 +143,7 @@ export class BadgeHubDataPostgresAdapter implements BadgeHubDataPort { throw new Error("Method not implemented."); } - writeProjectZip( + writeDraftProjectZip( projectSlug: string, zipContent: Uint8Array ): Promise { diff --git a/src/domain/BadgeHubDataPort.ts b/src/domain/BadgeHubDataPort.ts index 951a382..276f12e 100644 --- a/src/domain/BadgeHubDataPort.ts +++ b/src/domain/BadgeHubDataPort.ts @@ -20,13 +20,13 @@ export interface BadgeHubDataPort { deleteProject(projectSlug: ProjectSlug): Promise; - writeFile( + writeDraftFile( projectSlug: ProjectSlug, filePath: string, contents: string | Uint8Array ): Promise; - writeProjectZip( + writeDraftProjectZip( projectSlug: string, zipContent: Uint8Array ): Promise; diff --git a/src/setupPopulateDBApi.ts b/src/setupPopulateDBApi.ts index f6c3409..585040f 100644 --- a/src/setupPopulateDBApi.ts +++ b/src/setupPopulateDBApi.ts @@ -332,7 +332,7 @@ async function insertProjects(client: pg.PoolClient, userCount: number) { updated_at: updatedAt, }; - await badgeHubAdapter.writeFile( + await badgeHubAdapter.writeDraftFile( inserted.slug, "metadata.json", JSON.stringify(appMetadata) From a57a2100320a8828dfd940888d2c6404b1336368 Mon Sep 17 00:00:00 2001 From: Francis Duvivier Date: Wed, 4 Dec 2024 19:22:19 +0100 Subject: [PATCH 9/9] Remove insertUser api Since we wer currently just inserting the unsalted password into the DB. --- src/controllers/private-rest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/private-rest.ts b/src/controllers/private-rest.ts index 0e68a5c..3d50bc0 100644 --- a/src/controllers/private-rest.ts +++ b/src/controllers/private-rest.ts @@ -30,7 +30,8 @@ export class PrivateRestController { @Path() userId: DBUser["id"], @Body() props: UserProps ): Promise { - await this.badgeHubData.insertUser({ ...props, id: userId }); + // TODO implement with proper password handling (salting, hashing, ...) + throw new Error("Not implemented"); } /**