diff --git a/package.json b/package.json index ebb1c98..f1566f6 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "eslint": "^8.23.1", "knex": "^2.3.0", "mysql2": "^2.3.3", + "nodemon": "^2.0.20", "pg": "^8.8.0", "pnpm": "^7.11.0", "prettier": "^2.7.1", @@ -109,5 +110,14 @@ "bracketSpacing": true, "arrowParens": "always", "printWidth": 100 + }, + "nodemonConfig": { + "watch": [ + "src", + "tests" + ], + "execMap": { + "ts": "ts-node-esm" + } } } diff --git a/packages/core/src/builder.ts b/packages/core/src/builder.ts deleted file mode 100644 index 4a07fd7..0000000 --- a/packages/core/src/builder.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { faker } from '@faker-js/faker' -import defu from 'defu' -import humps from 'humps' -import { factorioConfig } from './config' -import type { FactoryModel } from './model' -import type { FactoryExtractGeneric, WithCallback } from './contracts' -import type { Knex } from 'knex' - -const { camelizeKeys, decamelizeKeys } = humps - -export class Builder< - Factory extends FactoryModel, - Model extends Record = FactoryExtractGeneric, - States = FactoryExtractGeneric -> { - constructor(private factory: Factory) {} - - /** - * The attributes that will be merged for the next created models. - */ - private mergeInput: Partial | Partial[] = [] - - /** - * States to apply. - */ - private appliedStates: Set = new Set() - - /** - * Relationships to create - */ - private appliedRelationships: { name: string; count?: number; callback?: WithCallback }[] = [] - - /** - * Ensure a knex connection is alive - */ - private ensureFactoryConnectionIsSet(knex: Knex | null): knex is Knex { - if (knex) return true - throw new Error('You must set a connection to the database before using the factory') - } - - /** - * Get the merge attributes for the given index. - */ - private getMergeAttributes(index: number) { - if (Array.isArray(this.mergeInput)) { - return this.mergeInput[index] || {} - } - - return this.mergeInput - } - - /** - * Merge custom attributes on final rows - */ - private mergeAttributes(rows: Record[]) { - if (Array.isArray(this.mergeInput)) { - return rows.map((row, index) => defu(this.getMergeAttributes(index), row)) - } - - return rows.map((row) => defu(this.mergeInput, row)) - } - - /** - * Apply pending states on each row - */ - private applyStates(rows: Record[]) { - const states = Array.from(this.appliedStates) - - for (const state of states) { - if (typeof state !== 'string') { - throw new TypeError('You must provide a state name to apply') - } - - const stateCallback = this.factory.states[state] - - if (!stateCallback) { - throw new Error(`The state "${state}" does not exist on the factory`) - } - - rows = rows.map((row) => defu(stateCallback(row), row)) - } - - return rows - } - - /** - * Unwrap factory fields that are functions. - */ - private async unwrapComputedFields(rows: Record[]) { - for (const row of rows) { - for (const key in row) { - if (typeof row[key] === 'function') { - const fn = row[key] - const result = await fn() - - row[key] = result?.id || result - } - } - } - } - - /** - * Hydrate relationships into the models before returning them to - * the user - */ - private hydrateRelationships( - models: Record[], - type: string, - relationship: { name: string; count?: number }, - relations: any[] - ) { - for (const model of models) { - if (type === 'has-one') { - model[relationship.name] = relations.shift() - } else if (type === 'has-many') { - model[relationship.name] = relations.splice(0, relationship.count || 1) - } - } - } - - /** - * Persist relationships to database and return them - */ - private async createRelationships(models: Record[]) { - for (const relationship of this.appliedRelationships) { - const { name, count, callback } = relationship - const { factory, foreignKey, localKey, type } = this.factory.relations[name]! - - if (callback) callback(factory) - - const mergeAttributes = models.reduce((acc, model) => { - for (let i = 0; i < (count || 1); i++) { - let mergeInput = factory.mergeInput - if (Array.isArray(factory.mergeInput)) { - mergeInput = factory.getMergeAttributes(i) - } - - acc.push({ ...mergeInput, [foreignKey]: model[localKey] }) - } - return acc - }, []) - - const relations = await factory - .merge(mergeAttributes) - .createMany((count || 1) * models.length) - - this.hydrateRelationships(models, type, relationship, relations) - } - } - - /** - * Reset the builder to its initial state. - */ - private resetBuilder() { - this.mergeInput = [] - this.appliedStates = new Set() - this.appliedRelationships = [] - - Object.values(this.factory.relations).forEach((relation) => relation.factory.resetBuilder()) - } - - /** - * Store a merge data that will be used when creating a new model. - */ - public merge(data: Partial | Partial[]) { - this.mergeInput = data - return this - } - - /** - * Create a new model and persist it to the database. - */ - public async create(): Promise { - this.ensureFactoryConnectionIsSet(factorioConfig.knex) - const res = await this.createMany(1) - return res[0]! - } - - /** - * Apply a registered state - */ - public apply(state: States) { - this.appliedStates.add(state) - return this - } - - /** - * Apply a relationship - */ - public with(name: string, count = 1, callback?: WithCallback) { - const relationship = this.factory.relations[name] - - if (!relationship) { - throw new Error(`The relationship "${name}" does not exist on the factory`) - } - - this.appliedRelationships.push({ name, count, callback }) - return this - } - - /** - * Create multiple models and persist them to the database. - */ - public async createMany(count: number): Promise { - this.ensureFactoryConnectionIsSet(factorioConfig.knex) - - let rows: Record[] = [] - const { tableName } = this.factory.callback({ faker }) - - for (let i = 0; i < count; i++) { - rows.push(this.factory.callback({ faker }).fields) - } - - /** - * Apply merge attributes, states, and computed fields before - * inserting - */ - rows = this.mergeAttributes(rows) - rows = this.applyStates(rows) - await this.unwrapComputedFields(rows) - - /** - * Insert rows - */ - const res = await factorioConfig - .knex!.insert(decamelizeKeys(rows)) - .into(tableName) - .returning('*') - - /** - * Create post relationships - */ - await this.createRelationships(res) - - this.resetBuilder() - - return res.map((row) => camelizeKeys(row)) as Model[] - } -} diff --git a/packages/core/src/builder/builder.ts b/packages/core/src/builder/builder.ts new file mode 100644 index 0000000..196cea4 --- /dev/null +++ b/packages/core/src/builder/builder.ts @@ -0,0 +1,187 @@ +import { faker } from '@faker-js/faker' +import defu from 'defu' +import humps from 'humps' +import { factorioConfig } from '../config' +import { RelationshipBuilder } from './relationship_builder' +import { StatesManager } from './states_manager' +import type { FactoryModel } from '../model' +import type { FactoryExtractGeneric, WithCallback } from '../contracts' +import type { Knex } from 'knex' + +const { camelizeKeys, decamelizeKeys } = humps + +export class Builder< + Factory extends FactoryModel, + Model extends Record = FactoryExtractGeneric, + States = FactoryExtractGeneric +> { + private relationshipBuilder: RelationshipBuilder + private statesManager: StatesManager + + constructor(private factory: Factory) { + this.relationshipBuilder = new RelationshipBuilder(factory) + this.statesManager = new StatesManager(factory) + } + + /** + * The attributes that will be merged for the next created models. + */ + private mergeInput: Partial | Partial[] = [] + + /** + * Ensure a knex connection is alive + */ + private ensureFactoryConnectionIsSet(knex: Knex | null): knex is Knex { + if (knex) return true + throw new Error('You must set a connection to the database before using the factory') + } + + /** + * Get the merge attributes for the given index. + */ + public getMergeAttributes(index: number) { + if (Array.isArray(this.mergeInput)) { + return this.mergeInput[index] || {} + } + + return this.mergeInput + } + + /** + * Merge custom attributes on final rows + */ + private mergeAttributes(rows: Record[]) { + if (Array.isArray(this.mergeInput)) { + return rows.map((row, index) => defu(this.getMergeAttributes(index), row)) + } + + return rows.map((row) => defu(this.mergeInput, row)) + } + + /** + * Unwrap factory fields that are functions. + */ + private async unwrapComputedFields(rows: Record[]) { + const unwrappings = rows.map(async (row) => { + const unwrappedRow: Record = {} + + for (const [key, value] of Object.entries(row)) { + if (typeof value === 'function') { + const fn = row[key] + const result = await fn() + + unwrappedRow[key] = result?.id || result + } else { + unwrappedRow[key] = value + } + } + + return unwrappedRow + }) + + return Promise.all(unwrappings) + } + + /** + * Reset the builder to its initial state. + */ + private resetBuilder() { + this.mergeInput = [] + this.statesManager.reset() + this.relationshipBuilder.reset() + + Object.values(this.factory.relations).forEach((relation) => relation.factory.resetBuilder()) + } + + /** + * Store a merge data that will be used when creating a new model. + */ + public merge(data: Partial | Partial[]) { + this.mergeInput = data + return this + } + + /** + * Create a new model and persist it to the database. + */ + public async create(): Promise { + this.ensureFactoryConnectionIsSet(factorioConfig.knex) + const res = await this.createMany(1) + return res[0]! + } + + /** + * Apply a registered state + */ + public apply(state: States) { + this.statesManager.register(state) + return this + } + + /** + * Apply a relationship + */ + public with(name: string, count = 1, callback?: WithCallback) { + this.relationshipBuilder.apply(name, count, callback) + return this + } + + /** + * Create multiple models and persist them to the database. + */ + public async createMany(count: number): Promise { + this.ensureFactoryConnectionIsSet(factorioConfig.knex) + + let models: Record[] = [] + const { tableName } = this.factory.callback({ faker }) + + /** + * Generate fields for each row by calling the factory callback + */ + for (let i = 0; i < count; i++) { + models.push(this.factory.callback({ faker }).fields) + } + + /** + * Apply merge attributes + */ + models = this.mergeAttributes(models) + + /** + * Apply the states + */ + models = this.statesManager.applyStates(models) + + /** + * Unwrap computed fields by calling their callbacks + */ + models = await this.unwrapComputedFields(models) + + /** + * We now create the belongsTo relationships + */ + await this.relationshipBuilder.createPre(models) + + /** + * Insert rows + */ + const res = await factorioConfig + .knex!.insert(decamelizeKeys(models)) + .into(tableName) + .returning('*') + + /** + * Create post relationships + */ + await this.relationshipBuilder.createPost(res) + + /** + * Hydrate pre relationships into the result + */ + const finalModels = this.relationshipBuilder.postHydrate(res) + + this.resetBuilder() + + return finalModels.map((model) => camelizeKeys(model)) as Model[] + } +} diff --git a/packages/core/src/builder/relationship_builder.ts b/packages/core/src/builder/relationship_builder.ts new file mode 100644 index 0000000..2e61a96 --- /dev/null +++ b/packages/core/src/builder/relationship_builder.ts @@ -0,0 +1,135 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import type { FactoryModel } from '../model.js' +import type { WithCallback } from '../contracts.js' + +export class RelationshipBuilder { + constructor(private factory: FactoryModel) {} + + /** + * Relationships to create + */ + private appliedRelationships: { name: string; count?: number; callback?: WithCallback }[] = [] + + /** + * Keep track of models created by the belongsTo relationship + * in order to hydrate after the main model is created. + */ + private preModels: Record[] = [] + + /** + * Hydrate relationships into the models before returning them to + * the user + */ + private hydrateRelationships( + models: Record[], + type: string, + relationship: { name: string; count?: number }, + relations: any[] + ) { + for (const model of models) { + if (type === 'has-one') { + model[relationship.name] = relations.shift() + } else if (type === 'has-many') { + model[relationship.name] = relations.splice(0, relationship.count || 1) + } else if (type === 'belongs-to') { + model[relationship.name] = relations.shift() + } + } + } + + /** + * Filter relationships by their type. + */ + private filterRelationshipsByType(type: 'pre' | 'post') { + return this.appliedRelationships.filter((relationship) => { + const meta = this.factory.relations[relationship.name]! + if (type === 'pre') { + return meta.type === 'belongs-to' + } + + return meta.type !== 'belongs-to' + }) + } + + /** + * Create post relationships ( hasOne, hasMany ), and persist them + */ + public async createPost(models: Record[]) { + const relationships = this.filterRelationshipsByType('post') + + for (const relationship of relationships) { + const { name, count, callback } = relationship + const { factory, foreignKey, localKey, type } = this.factory.relations[name]! + + if (callback) callback(factory) + + const mergeAttributes = models.reduce((acc, model) => { + for (let i = 0; i < (count || 1); i++) { + const mergeInput = factory.getMergeAttributes(i) + acc.push({ ...mergeInput, [foreignKey]: model[localKey] }) + } + return acc + }, []) + + const relations = await factory + .merge(mergeAttributes) + .createMany((count || 1) * models.length) + + this.hydrateRelationships(models, type, relationship, relations) + } + } + + /** + * Create pre relationships ( belongsTo ), and persist them + */ + public async createPre(models: Record[]) { + const relationships = this.filterRelationshipsByType('pre') + + for (const relationship of relationships) { + const { name, count, callback } = relationship + const { factory, foreignKey, localKey } = this.factory.relations[name]! + + if (callback) callback(factory) + + const relations = await factory.createMany((count || 1) * models.length) + models.forEach((model, index) => (model[foreignKey] = relations[index][localKey])) + + this.preModels = this.preModels.concat({ + name, + count, + relations, + }) + } + } + + /** + * Hydrate the pre models into the main models + */ + public postHydrate(models: Record[]) { + for (const { name, count, relations } of this.preModels) { + this.hydrateRelationships(models, 'belongs-to', { name, count }, relations) + } + return models + } + + /** + * Register a relationship to be created + */ + public apply(name: string, count?: number, callback?: WithCallback) { + const relationship = this.factory.relations[name] + + if (!relationship) { + throw new Error(`The relationship "${name}" does not exist on the factory`) + } + + this.appliedRelationships.push({ name, count, callback }) + } + + /** + * Reset the builder to its initial state. + */ + public reset() { + this.appliedRelationships = [] + this.preModels = [] + } +} diff --git a/packages/core/src/builder/states_manager.ts b/packages/core/src/builder/states_manager.ts new file mode 100644 index 0000000..6e65c08 --- /dev/null +++ b/packages/core/src/builder/states_manager.ts @@ -0,0 +1,56 @@ +import defu from 'defu' +import type { FactoryModel } from '../model' + +export class StatesManager { + constructor(private factory: FactoryModel) {} + + /** + * Registered states that need to be applied on the + * next created models. + */ + private registeredStates: Set = new Set() + + /** + * Get the callback for the given state. + */ + getStateCallback(state: States) { + if (typeof state !== 'string') { + throw new TypeError('You must provide a state name to apply') + } + + const stateCallback = this.factory.states[state] + if (!stateCallback) { + throw new Error(`The state "${state}" does not exist on the factory`) + } + + return stateCallback + } + + /** + * Apply the registered states on the given rows. + */ + public applyStates(rows: Record[]) { + const states = Array.from(this.registeredStates) + + for (const state of states) { + const stateCallback = this.getStateCallback(state) + rows = rows.map((row) => defu(stateCallback(row), row)) + } + + return rows + } + + /** + * Register a state to be applied on the next created models. + */ + public register(state: States) { + this.registeredStates.add(state) + } + + /** + * Unregister all states. + */ + public reset() { + this.registeredStates = new Set() + } +} diff --git a/packages/core/src/contracts.ts b/packages/core/src/contracts.ts index fab3c6d..c8bec92 100644 --- a/packages/core/src/contracts.ts +++ b/packages/core/src/contracts.ts @@ -1,4 +1,4 @@ -import type { Builder } from './builder.js' +import type { Builder } from './builder/builder.js' import type { FactoryModel } from './model' import type { faker } from '@faker-js/faker' import type { Knex } from 'knex' diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 279abb0..5ce5f37 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,8 +1,8 @@ -import { Builder } from './builder' +import { Builder } from './builder/builder' import type { DefineFactoryCallback, DefineStateCallback } from './contracts' interface HasOneMeta { - type: 'has-one' | 'has-many' + type: 'has-one' | 'has-many' | 'belongs-to' localKey: string foreignKey: string factory: Builder @@ -57,6 +57,14 @@ export class FactoryModel, States extends stri return this } + /** + * Add belongsTo relationship + */ + public belongsTo(name: string, meta: Omit) { + this.relations[name] = { ...meta, type: 'belongs-to' } + return this + } + /** * Returns the Builder */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97b300a..99ebba0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,7 @@ specifiers: eslint: ^8.23.1 knex: ^2.3.0 mysql2: ^2.3.3 + nodemon: ^2.0.20 pg: ^8.8.0 pnpm: ^7.11.0 prettier: ^2.7.1 @@ -43,6 +44,7 @@ devDependencies: eslint: 8.23.1 knex: 2.3.0_326n2ufkg7hzishnsgt5jjf5qy mysql2: 2.3.3 + nodemon: 2.0.20 pg: 8.8.0 pnpm: 7.11.0 prettier: 2.7.1 @@ -1465,6 +1467,14 @@ packages: engines: {node: '>=10'} dev: true + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + /api-contract-validator/2.2.8: resolution: {integrity: sha512-YM3rMcrIp8Thf/WWbVBXBGX793Mm3Phw2pn3VbJpiZkpeTCTtF10huKPrzQ2gSIaK5GjAhTRJMAOyf+rsS7MAw==} engines: {node: '>=8'} @@ -1631,6 +1641,11 @@ packages: prebuild-install: 7.1.1 dev: true + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + /bindings/1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} dependencies: @@ -1896,6 +1911,21 @@ packages: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /chownr/1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} dev: true @@ -2141,6 +2171,18 @@ packages: ms: 2.1.3 dev: true + /debug/3.2.7_supports-color@5.5.0: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: true + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -3893,6 +3935,10 @@ packages: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true + /ignore-by-default/1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + /ignore/5.2.0: resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} engines: {node: '>= 4'} @@ -3987,6 +4033,13 @@ packages: has-bigints: 1.0.2 dev: true + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + /is-boolean-object/1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} @@ -5039,6 +5092,30 @@ packages: resolution: {integrity: sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==} dev: true + /nodemon/2.0.20: + resolution: {integrity: sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==} + engines: {node: '>=8.10.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 3.2.7_supports-color@5.5.0 + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 5.7.1 + simple-update-notifier: 1.0.7 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt/1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + /nopt/5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -5056,6 +5133,11 @@ packages: validate-npm-package-license: 3.0.4 dev: true + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + /npmlog/5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} dependencies: @@ -5604,6 +5686,10 @@ packages: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true + /pstree.remy/1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + /pump/3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -5666,6 +5752,13 @@ packages: util-deprecate: 1.0.2 dev: true + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + /rechoir/0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} @@ -5836,6 +5929,11 @@ packages: hasBin: true dev: true + /semver/7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + dev: true + /semver/7.3.7: resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} engines: {node: '>=10'} @@ -5903,6 +6001,13 @@ packages: simple-concat: 1.0.1 dev: true + /simple-update-notifier/1.0.7: + resolution: {integrity: sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==} + engines: {node: '>=8.10.0'} + dependencies: + semver: 7.0.0 + dev: true + /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -6267,6 +6372,13 @@ packages: is-number: 7.0.0 dev: true + /touch/3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + /tr46/0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true @@ -6426,6 +6538,10 @@ packages: - supports-color dev: true + /undefsafe/2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + /unique-filename/1.1.1: resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} dependencies: diff --git a/tests-helpers/db.ts b/tests-helpers/db.ts index 30d4c01..282fa8d 100644 --- a/tests-helpers/db.ts +++ b/tests-helpers/db.ts @@ -34,6 +34,7 @@ export const setupDb = async () => { await connection.schema.dropTableIfExists('post') await connection.schema.dropTableIfExists('account') await connection.schema.dropTableIfExists('user') + await connection.schema.dropTableIfExists('admin') await connection.schema.createTable('user', (table) => { table.increments('id').primary() @@ -41,6 +42,12 @@ export const setupDb = async () => { table.string('password') }) + await connection.schema.createTable('admin', (table) => { + table.increments('id').primary() + table.string('email') + table.string('password') + }) + await connection.schema.createTable('profile', (table) => { table.increments('id').primary() table.string('email') @@ -62,7 +69,9 @@ export const setupDb = async () => { table.increments('id').primary() table.string('name') table.integer('user_id').unsigned() + table.integer('admin_id').unsigned().nullable() table.foreign('user_id').references('id').inTable('user').onDelete('CASCADE') + table.foreign('admin_id').references('id').inTable('admin').onDelete('CASCADE') }) } diff --git a/tests-helpers/setup.ts b/tests-helpers/setup.ts index 945303d..8a43619 100644 --- a/tests-helpers/setup.ts +++ b/tests-helpers/setup.ts @@ -22,13 +22,21 @@ export const UserFactory = defineFactory(({ faker }) => ({ tableName: 'user', fields: { id: faker.datatype.number() }, })) + .state('easyPassword', () => ({ password: 'easy' })) + .state('easyEmail', () => ({ email: 'easy@easy.com' })) .hasOne('profile', { foreignKey: 'user_id', localKey: 'id', factory: ProfileFactory }) .hasMany('posts', { foreignKey: 'user_id', localKey: 'id', factory: PostFactory }) .build() +export const AdminFactory = defineFactory(({ faker }) => ({ + tableName: 'admin', + fields: { id: faker.datatype.number() }, +})).build() + export const AccountFactory = defineFactory(({ faker }) => ({ tableName: 'account', fields: { name: faker.commerce.productName() }, })) - // .belongsTo('user', { foreignKey: 'user_id', localKey: 'id', factory: UserFactory }) + .belongsTo('user', { foreignKey: 'user_id', localKey: 'id', factory: UserFactory }) + .belongsTo('admin', { foreignKey: 'admin_id', localKey: 'id', factory: AdminFactory }) .build() diff --git a/tests/belongs_to.spec.ts b/tests/belongs_to.spec.ts new file mode 100644 index 0000000..4c6028d --- /dev/null +++ b/tests/belongs_to.spec.ts @@ -0,0 +1,74 @@ +import { test } from '@japa/runner' +import { DatabaseUtils } from '@julr/japa-database-plugin' +import { AccountFactory } from '../tests-helpers/setup.js' +import { setupDb } from '../tests-helpers/db.js' + +test.group('BelongsTo', (group) => { + group.setup(async () => setupDb()) + group.each.setup(() => DatabaseUtils.refreshDatabase()) + + test('Basic', async ({ database, expect }) => { + const account = await AccountFactory.with('user').create() + + expect(account.userId).toBeDefined() + expect(account.user).toBeDefined() + expect(account.user.id).toBeDefined() + expect(account.user.id).toStrictEqual(account.userId) + + await database.assertHas('account', { user_id: account.user.id }, 1) + await database.assertHas('user', { id: account.userId }, 1) + }) + + test('createMany', async ({ database }) => { + const [accountA, accountB] = await AccountFactory.with('user').createMany(2) + + await Promise.all([ + database.assertCount('user', 2), + database.assertCount('account', 2), + + database.assertHas('account', { user_id: accountA!.user.id }, 1), + database.assertHas('account', { user_id: accountB!.user.id }, 1), + + database.assertHas('user', { id: accountA!.user.id }, 1), + database.assertHas('user', { id: accountB!.user.id }, 1), + ]) + }) + + test('Chaining with', async ({ database }) => { + const account = await AccountFactory.with('user').with('admin').create() + + await Promise.all([ + database.assertCount('user', 1), + database.assertCount('admin', 1), + database.assertCount('account', 1), + + database.assertHas('account', { user_id: account.user.id }, 1), + database.assertHas('account', { admin_id: account.admin.id }, 1), + database.assertHas('user', { id: account.user.id }, 1), + database.assertHas('admin', { id: account.admin.id }, 1), + ]) + }) + + test('Chaining with - callback', async ({ database }) => { + const account = await AccountFactory.with('user', 1, (user) => user.merge({ email: 'bonjour' })) + .with('admin', 1, (admin) => admin.merge({ email: 'admin' })) + .create() + + await Promise.all([ + database.assertHas('user', { email: 'bonjour' }, 1), + database.assertHas('admin', { email: 'admin' }, 1), + database.assertHas('account', { user_id: account.user.id, admin_id: account.admin.id }, 1), + ]) + }) + + test('With - state', async ({ database }) => { + const account = await AccountFactory.with('user', 1, (user) => + user.apply('easyPassword').apply('easyEmail') + ).create() + + await Promise.all([ + database.assertHas('user', { password: 'easy', email: 'easy@easy.com' }), + database.assertHas('account', { user_id: account.user.id }, 1), + ]) + }) +})