From 231629f0dda0f8a6105cd999ca1691dc30b31415 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 28 Apr 2024 00:45:10 +0200 Subject: [PATCH] chore: create user, collection and link models, migrations and seeders --- Makefile | 3 ++ app/controllers/collections_controller.ts | 3 ++ app/models/app_base_model.ts | 26 +++++++++- app/models/collection.ts | 33 +++++++++++++ app/models/link.ts | 31 ++++++++++++ app/models/user.ts | 34 +++----------- config/database.ts | 3 ++ .../1714218548323_create_users_table.ts | 3 +- .../1714253983443_create_collections_table.ts | 32 +++++++++++++ .../1714254076754_create_links_table.ts | 25 ++++++++++ database/seeders/collection_seeder.ts | 34 ++++++++++++++ database/seeders/link_seeder.ts | 47 +++++++++++++++++++ database/seeders/main/index_seeder.ts | 33 +++++++++++++ database/seeders/user_seeder.ts | 28 +++++++++++ docker-config/servers_pgadmin.json | 4 +- package-lock.json | 1 + package.json | 3 +- 17 files changed, 311 insertions(+), 32 deletions(-) create mode 100644 app/controllers/collections_controller.ts create mode 100644 app/models/collection.ts create mode 100644 app/models/link.ts create mode 100644 database/migrations/1714253983443_create_collections_table.ts create mode 100644 database/migrations/1714254076754_create_links_table.ts create mode 100644 database/seeders/collection_seeder.ts create mode 100644 database/seeders/link_seeder.ts create mode 100644 database/seeders/main/index_seeder.ts create mode 100644 database/seeders/user_seeder.ts diff --git a/Makefile b/Makefile index 5567387..7c5aa4c 100644 --- a/Makefile +++ b/Makefile @@ -7,3 +7,6 @@ dev: prod: docker compose -f dev.docker-compose.yml down docker compose up -d --build --wait + +seed: + node ace db:seed diff --git a/app/controllers/collections_controller.ts b/app/controllers/collections_controller.ts new file mode 100644 index 0000000..005d959 --- /dev/null +++ b/app/controllers/collections_controller.ts @@ -0,0 +1,3 @@ +// import type { HttpContext } from '@adonisjs/core/http'; + +export default class CollectionsController {} diff --git a/app/models/app_base_model.ts b/app/models/app_base_model.ts index ae28a5b..7a0f458 100644 --- a/app/models/app_base_model.ts +++ b/app/models/app_base_model.ts @@ -1,5 +1,29 @@ -import { BaseModel, CamelCaseNamingStrategy } from '@adonisjs/lucid/orm'; +import { BaseModel, CamelCaseNamingStrategy, beforeCreate, column } from '@adonisjs/lucid/orm'; +import { DateTime } from 'luxon'; +import { v4 as uuidv4 } from 'uuid'; export default class AppBaseModel extends BaseModel { static namingStrategy = new CamelCaseNamingStrategy(); + static selfAssignPrimaryKey = true; + + @column({ isPrimary: true }) + declare id: string; // UUID + + @column.dateTime({ + autoCreate: true, + serializeAs: 'createdAt', + }) + declare createdAt: DateTime; + + @column.dateTime({ + autoCreate: true, + autoUpdate: true, + serializeAs: 'updatedAt', + }) + declare updatedAt: DateTime; + + @beforeCreate() + static assignUuid(item: any) { + item.id = uuidv4(); + } } diff --git a/app/models/collection.ts b/app/models/collection.ts new file mode 100644 index 0000000..6576085 --- /dev/null +++ b/app/models/collection.ts @@ -0,0 +1,33 @@ +import AppBaseModel from '#models/app_base_model'; +import Link from '#models/link'; +import User from '#models/user'; +import { belongsTo, column, manyToMany } from '@adonisjs/lucid/orm'; +import type { BelongsTo, ManyToMany } from '@adonisjs/lucid/types/relations'; + +export default class Collection extends AppBaseModel { + @column() + declare name: string; + + @column() + declare description: string | null; + + @column() + declare visibility: Visibility; + + @column() + declare nextId: string; + + @column() + declare authorId: string; + + @belongsTo(() => User, { foreignKey: 'authorId' }) + declare author: BelongsTo; + + @manyToMany(() => Link) + declare links: ManyToMany; +} + +export enum Visibility { + PUBLIC = 'PUBLIC', + PRIVATE = 'PRIVATE', +} diff --git a/app/models/link.ts b/app/models/link.ts new file mode 100644 index 0000000..ed0646c --- /dev/null +++ b/app/models/link.ts @@ -0,0 +1,31 @@ +import AppBaseModel from '#models/app_base_model'; +import Collection from '#models/collection'; +import User from '#models/user'; +import { belongsTo, column } from '@adonisjs/lucid/orm'; +import type { BelongsTo } from '@adonisjs/lucid/types/relations'; + +export default class Link extends AppBaseModel { + @column() + declare name: string; + + @column() + declare description: string; + + @column() + declare url: string; + + @column() + declare favorite: boolean; + + @column() + declare collectionId: string; + + @belongsTo(() => Collection, { foreignKey: 'collectionId' }) + declare collection: BelongsTo; + + @column() + declare authorId: string; + + @belongsTo(() => User, { foreignKey: 'authorId' }) + declare author: BelongsTo; +} diff --git a/app/models/user.ts b/app/models/user.ts index c72b832..cb19d47 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,15 +1,10 @@ +import Collection from '#models/collection'; import type { GoogleToken } from '@adonisjs/ally/types'; -import { beforeCreate, column } from '@adonisjs/lucid/orm'; -import { DateTime } from 'luxon'; -import { v4 as uuidv4 } from 'uuid'; +import { column, manyToMany } from '@adonisjs/lucid/orm'; +import type { ManyToMany } from '@adonisjs/lucid/types/relations'; import AppBaseModel from './app_base_model.js'; export default class User extends AppBaseModel { - static selfAssignPrimaryKey = true; - - @column({ isPrimary: true }) - declare id: string; // UUID - @column() declare email: string; @@ -22,6 +17,7 @@ export default class User extends AppBaseModel { @column({ serializeAs: 'avatarUrl' }) declare avatarUrl: string; + @column() declare isAdmin: boolean; @column({ serializeAs: null }) @@ -30,24 +26,8 @@ export default class User extends AppBaseModel { @column({ serializeAs: null }) declare providerId: string; - @column.dateTime({ - autoCreate: true, - serialize: (value: DateTime) => value.toISODate(), - serializeAs: 'createdAt', + @manyToMany(() => Collection, { + relatedKey: 'authorId', }) - declare createdAt: DateTime; - - @column.dateTime({ - autoCreate: true, - autoUpdate: true, - serialize: (value: DateTime) => value.toISODate(), - serializeAs: 'updatedAt', - }) - declare updatedAt: DateTime; - - @beforeCreate() - static assignUuid(user: User) { - user.id = uuidv4(); - user.isAdmin = false; - } + declare collections: ManyToMany; } diff --git a/config/database.ts b/config/database.ts index 6de34c0..cdf5995 100644 --- a/config/database.ts +++ b/config/database.ts @@ -17,6 +17,9 @@ const dbConfig = defineConfig({ naturalSort: true, paths: ['database/migrations'], }, + seeders: { + paths: ['./database/seeders/main'], + }, }, }, }); diff --git a/database/migrations/1714218548323_create_users_table.ts b/database/migrations/1714218548323_create_users_table.ts index a4f3f24..a65e692 100644 --- a/database/migrations/1714218548323_create_users_table.ts +++ b/database/migrations/1714218548323_create_users_table.ts @@ -5,7 +5,8 @@ export default class extends BaseSchema { async up() { this.schema.createTable(this.tableName, (table) => { - table.string('id').notNullable(); + table.uuid('id').notNullable(); + table.string('email', 254).notNullable().unique(); table.string('name', 254).notNullable(); table.string('nick_name', 254).notNullable(); diff --git a/database/migrations/1714253983443_create_collections_table.ts b/database/migrations/1714253983443_create_collections_table.ts new file mode 100644 index 0000000..8c41068 --- /dev/null +++ b/database/migrations/1714253983443_create_collections_table.ts @@ -0,0 +1,32 @@ +import { Visibility } from '#models/collection'; +import { BaseSchema } from '@adonisjs/lucid/schema'; + +export default class extends BaseSchema { + protected tableName = 'collections'; + private visibilityEnumName = 'collection_visibility'; + + async up() { + this.schema.raw(`DROP TYPE IF EXISTS ${this.visibilityEnumName}`); + this.schema.createTable(this.tableName, (table) => { + table.uuid('id').notNullable(); + + table.string('name', 254).notNullable(); + table.string('description', 254); + table.uuid('next_id').notNullable(); + table.uuid('author_id').notNullable(); + table.enum('visibility', Object.values(Visibility), { + useNative: true, + enumName: this.visibilityEnumName, + existingType: false, + }); + + table.timestamp('created_at'); + table.timestamp('updated_at'); + }); + } + + async down() { + this.schema.raw(`DROP TYPE IF EXISTS ${this.visibilityEnumName}`); + this.schema.dropTable(this.tableName); + } +} diff --git a/database/migrations/1714254076754_create_links_table.ts b/database/migrations/1714254076754_create_links_table.ts new file mode 100644 index 0000000..2ee0e24 --- /dev/null +++ b/database/migrations/1714254076754_create_links_table.ts @@ -0,0 +1,25 @@ +import { BaseSchema } from '@adonisjs/lucid/schema'; + +export default class extends BaseSchema { + protected tableName = 'links'; + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.uuid('id').notNullable(); + + table.string('name', 254).notNullable(); + table.string('description', 254); + table.text('url').notNullable(); + table.boolean('favorite').notNullable().defaultTo(0); + table.uuid('collection_id').notNullable(); + table.uuid('author_id').notNullable(); + + table.timestamp('created_at'); + table.timestamp('updated_at'); + }); + } + + async down() { + this.schema.dropTable(this.tableName); + } +} diff --git a/database/seeders/collection_seeder.ts b/database/seeders/collection_seeder.ts new file mode 100644 index 0000000..8204cb5 --- /dev/null +++ b/database/seeders/collection_seeder.ts @@ -0,0 +1,34 @@ +import Collection from '#models/collection'; +import User from '#models/user'; +import { BaseSeeder } from '@adonisjs/lucid/seeders'; +import { faker } from '@faker-js/faker'; + +export default class extends BaseSeeder { + static environment = ['development', 'testing']; + + async run() { + // eslint-disable-next-line unicorn/no-await-expression-member + const users = await getUserIds(); + + const collections = faker.helpers.multiple(() => createRandomCollection(users), { + count: 50, + }); + await Collection.createMany(collections); + } +} + +export async function getUserIds() { + const users = await User.all(); + return users.map(({ id }) => id); +} + +function createRandomCollection(userIds: User['id'][]) { + const authorId = faker.helpers.arrayElements(userIds, 1).at(0); + return { + id: faker.string.uuid(), + name: faker.string.alphanumeric({ length: { min: 5, max: 25 } }), + description: faker.string.alphanumeric({ length: { min: 0, max: 254 } }), + nextId: faker.string.uuid(), + authorId, + }; +} diff --git a/database/seeders/link_seeder.ts b/database/seeders/link_seeder.ts new file mode 100644 index 0000000..0a6c191 --- /dev/null +++ b/database/seeders/link_seeder.ts @@ -0,0 +1,47 @@ +import { getUserIds } from '#database/seeders/collection_seeder'; +import Collection from '#models/collection'; +import Link from '#models/link'; +import User from '#models/user'; +import { BaseSeeder } from '@adonisjs/lucid/seeders'; +import { faker } from '@faker-js/faker'; + +export default class extends BaseSeeder { + static environment = ['development', 'testing']; + + async run() { + // eslint-disable-next-line unicorn/no-await-expression-member + const users = await getUserIds(); + + const links = await Promise.all( + faker.helpers.multiple(async () => createRandomLink(users), { + count: 500, + }) + ); + await Link.createMany(links.filter((a) => typeof a !== 'undefined') as any); + } +} + +async function getCollectionIds(authorId: User['id']) { + const collection = await Collection.findManyBy('author_id', authorId); + return collection.map(({ id }) => id); +} + +async function createRandomLink(userIds: User['id'][]) { + const authorId = faker.helpers.arrayElements(userIds, 1).at(0)!; + const collections = await getCollectionIds(authorId); + + const collectionId = faker.helpers.arrayElements(collections, 1).at(0); + if (!collectionId) { + return undefined; + } + + return { + id: faker.string.uuid(), + name: faker.string.alphanumeric({ length: { min: 5, max: 25 } }), + description: faker.string.alphanumeric({ length: { min: 0, max: 254 } }), + url: faker.internet.url(), + favorite: faker.number.int({ min: 0, max: 1 }), + authorId, + collectionId, + }; +} diff --git a/database/seeders/main/index_seeder.ts b/database/seeders/main/index_seeder.ts new file mode 100644 index 0000000..e567f4c --- /dev/null +++ b/database/seeders/main/index_seeder.ts @@ -0,0 +1,33 @@ +import app from '@adonisjs/core/services/app'; +import logger from '@adonisjs/core/services/logger'; +import { BaseSeeder } from '@adonisjs/lucid/seeders'; + +export default class IndexSeeder extends BaseSeeder { + private async seed(Seeder: { default: typeof BaseSeeder }) { + /** + * Do not run when not in a environment specified in Seeder + */ + if ( + !Seeder.default.environment || + (!Seeder.default.environment.includes('development') && app.inDev) || + (!Seeder.default.environment.includes('testing') && app.inTest) || + (!Seeder.default.environment.includes('production') && app.inProduction) + ) { + return; + } + + await new Seeder.default(this.client).run(); + } + + async run() { + logger.info('Start user seed'); + await this.seed(await import('#database/seeders/user_seeder')); + logger.info('User seed done'); + logger.info('Collection user seed'); + await this.seed(await import('#database/seeders/collection_seeder')); + logger.info('Collection seed done'); + logger.info('Link user seed'); + await this.seed(await import('#database/seeders/link_seeder')); + logger.info('Link seed done'); + } +} diff --git a/database/seeders/user_seeder.ts b/database/seeders/user_seeder.ts new file mode 100644 index 0000000..91b22d8 --- /dev/null +++ b/database/seeders/user_seeder.ts @@ -0,0 +1,28 @@ +import User from '#models/user'; +import { GoogleToken } from '@adonisjs/ally/types'; +import { BaseSeeder } from '@adonisjs/lucid/seeders'; +import { faker } from '@faker-js/faker'; + +export default class extends BaseSeeder { + static environment = ['development', 'testing']; + + async run() { + const users = faker.helpers.multiple(createRandomUser, { + count: 25, + }); + await User.createMany(users); + } +} + +export function createRandomUser() { + return { + id: faker.string.uuid(), + email: faker.internet.email(), + name: faker.internet.userName(), + nickName: faker.internet.displayName(), + avatarUrl: faker.image.avatar(), + isAdmin: false, + providerId: faker.string.uuid(), + token: {} as GoogleToken, + }; +} diff --git a/docker-config/servers_pgadmin.json b/docker-config/servers_pgadmin.json index 34b253a..6466d70 100644 --- a/docker-config/servers_pgadmin.json +++ b/docker-config/servers_pgadmin.json @@ -2,7 +2,7 @@ "Servers": { "1": { "Name": "project", - "Group": "Server Group 1", + "Group": "Servers", "Port": 5432, "Username": "postgres", "Host": "postgres", @@ -10,4 +10,4 @@ "MaintenanceDB": "my-links" } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 720696e..308dcab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@adonisjs/eslint-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", "@emotion/babel-plugin": "^11.11.0", + "@faker-js/faker": "^8.4.1", "@japa/assert": "^3.0.0", "@japa/plugin-adonisjs": "^3.0.1", "@japa/runner": "^3.1.4", diff --git a/package.json b/package.json index 9c67f7d..084129b 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@adonisjs/eslint-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", "@emotion/babel-plugin": "^11.11.0", + "@faker-js/faker": "^8.4.1", "@japa/assert": "^3.0.0", "@japa/plugin-adonisjs": "^3.0.1", "@japa/runner": "^3.1.4", @@ -97,4 +98,4 @@ "arrowParens": "always", "printWidth": 100 } -} \ No newline at end of file +}