From f664a07b129a2e6dc8bca3b64daea0e62f8078e4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 11 Jan 2024 08:03:38 +0530 Subject: [PATCH 01/73] chore: publish under latest tag --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 32e1a0c1..436fce16 100644 --- a/package.json +++ b/package.json @@ -140,11 +140,11 @@ "prettier": "@adonisjs/prettier-config", "publishConfig": { "access": "public", - "tag": "next" + "tag": "latest" }, "np": { "message": "chore(release): %s", - "tag": "next", + "tag": "latest", "branch": "main", "anyBranch": false }, From 7d2d99a84bc803d4c19fcd852fd0a4ab788907a0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 11 Jan 2024 08:05:14 +0530 Subject: [PATCH 02/73] chore(release): 19.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 436fce16..c65d8d1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "19.0.0-8", + "version": "19.0.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From 71708b8ce95d7672206e6fdd6687159d00f9a861 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Mon, 22 Jan 2024 11:49:14 +0100 Subject: [PATCH 03/73] fix(seeds): correct log for ignored seed --- commands/db_seed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/db_seed.ts b/commands/db_seed.ts index 04c79eb4..a6b71ee3 100644 --- a/commands/db_seed.ts +++ b/commands/db_seed.ts @@ -81,7 +81,7 @@ export default class DbSeed extends BaseCommand { break case 'ignored': message = 'ignored ' - prefix = 'Enabled only in development environment' + prefix = `Disabled in "${this.app.getEnvironment()}" environment` color = 'dim' break case 'completed': From 718c68f867e14febf1ae20e0a881a07b225f1632 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 23 Jan 2024 10:53:04 +0530 Subject: [PATCH 04/73] chore: update dependencies --- package.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index c65d8d1d..86f1dc14 100644 --- a/package.json +++ b/package.json @@ -55,11 +55,11 @@ "index:commands": "adonis-kit index build/commands" }, "dependencies": { - "@adonisjs/presets": "^2.1.1", + "@adonisjs/presets": "^2.2.1", "@faker-js/faker": "^8.3.1", "@poppinss/hooks": "^7.2.2", "@poppinss/macroable": "^1.0.1", - "@poppinss/utils": "^6.7.0", + "@poppinss/utils": "^6.7.1", "fast-deep-equal": "^3.1.3", "igniculus": "^1.5.0", "kleur": "^4.1.5", @@ -71,40 +71,40 @@ "tarn": "^3.0.2" }, "devDependencies": { - "@adonisjs/assembler": "^7.0.0", - "@adonisjs/core": "^6.2.0", + "@adonisjs/assembler": "^7.1.0", + "@adonisjs/core": "^6.2.1", "@adonisjs/eslint-config": "^1.2.1", "@adonisjs/prettier-config": "^1.2.1", "@adonisjs/tsconfig": "^1.2.1", - "@commitlint/cli": "^18.4.4", - "@commitlint/config-conventional": "^18.4.4", + "@commitlint/cli": "^18.5.0", + "@commitlint/config-conventional": "^18.5.0", "@japa/assert": "^2.1.0", - "@japa/file-system": "^2.1.1", + "@japa/file-system": "^2.2.0", "@japa/runner": "^3.1.1", - "@swc/core": "^1.3.102", + "@swc/core": "^1.3.105", "@types/chance": "^1.1.6", - "@types/luxon": "^3.4.0", - "@types/node": "^20.10.8", + "@types/luxon": "^3.4.2", + "@types/node": "^20.11.5", "@types/pluralize": "^0.0.33", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.11", "@vinejs/vine": "^1.7.0", - "better-sqlite3": "^9.2.2", - "c8": "^9.0.0", + "better-sqlite3": "^9.3.0", + "c8": "^9.1.0", "chance": "^1.1.11", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "dotenv": "^16.0.3", + "dotenv": "^16.3.2", "eslint": "^8.56.0", "fs-extra": "^11.2.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "luxon": "^3.4.4", - "mysql2": "^3.7.0", + "mysql2": "^3.7.1", "np": "^9.2.0", "pg": "^8.11.0", - "prettier": "^3.1.1", + "prettier": "^3.2.4", "reflect-metadata": "^0.2.0", "sqlite3": "^5.1.7", "tedious": "^16.6.1", From c835376c6053522f0ae95436346b043ae1a7f580 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 23 Jan 2024 11:20:11 +0530 Subject: [PATCH 05/73] feat: change naming strategy to output camelCase key names in serialized output Breaking change: This commit changes the output of the APIs or anywhere else that model is serialized to JSON. You can switch the naming strategy to snake_case within your apps --- src/orm/base_model/index.ts | 3 +- src/orm/main.ts | 1 + src/orm/naming_strategies/camel_case.ts | 110 ++++++++++++++++++++++++ test/orm/base_model.spec.ts | 106 +++++++++++------------ test/orm/model_has_many.spec.ts | 16 ++-- test/orm/model_has_many_through.spec.ts | 16 ++-- test/orm/model_many_to_many.spec.ts | 16 ++-- 7 files changed, 190 insertions(+), 78 deletions(-) create mode 100644 src/orm/naming_strategies/camel_case.ts diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index f9574551..189d2ebb 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -64,6 +64,7 @@ import { import { SnakeCaseNamingStrategy } from '../naming_strategies/snake_case.js' import { LazyLoadAggregates } from '../relations/aggregates_loader/lazy_load.js' import * as errors from '../../errors.js' +import { CamelCaseNamingStrategy } from '../naming_strategies/camel_case.js' const MANY_RELATIONS = ['hasMany', 'manyToMany', 'hasManyThrough'] const DATE_TIME_TYPES = { @@ -99,7 +100,7 @@ class BaseModelImpl implements LucidRow { /** * Naming strategy for model properties */ - static namingStrategy = new SnakeCaseNamingStrategy() + static namingStrategy = new CamelCaseNamingStrategy() /** * Primary key is required to build relationships across models diff --git a/src/orm/main.ts b/src/orm/main.ts index 37519609..00931fa7 100644 --- a/src/orm/main.ts +++ b/src/orm/main.ts @@ -13,3 +13,4 @@ export * from './decorators/date_time.js' export { BaseModel, scope } from './base_model/index.js' export { ModelQueryBuilder } from './query_builder/index.js' export { SnakeCaseNamingStrategy } from './naming_strategies/snake_case.js' +export { CamelCaseNamingStrategy } from './naming_strategies/camel_case.js' diff --git a/src/orm/naming_strategies/camel_case.ts b/src/orm/naming_strategies/camel_case.ts new file mode 100644 index 00000000..b0a9abbb --- /dev/null +++ b/src/orm/naming_strategies/camel_case.ts @@ -0,0 +1,110 @@ +/* + * @adonisjs/lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import string from '@poppinss/utils/string' +import { ModelRelations } from '../../types/relations.js' +import { NamingStrategyContract, LucidModel } from '../../types/model.js' + +/** + * Camelcase naming strategy for the model to use camelcase keys + * for the serialized output. + */ +export class CamelCaseNamingStrategy implements NamingStrategyContract { + /** + * The default table name for the given model + */ + tableName(model: LucidModel): string { + return string.pluralize(string.snakeCase(model.name)) + } + + /** + * The database column name for a given model attribute + */ + columnName(_: LucidModel, attributeName: string): string { + return string.snakeCase(attributeName) + } + + /** + * The post serialization name for a given model attribute + */ + serializedName(_: LucidModel, attributeName: string): string { + return string.camelCase(attributeName) + } + + /** + * The local key for a given model relationship + */ + relationLocalKey( + relation: ModelRelations['__opaque_type'], + model: LucidModel, + relatedModel: LucidModel + ): string { + if (relation === 'belongsTo') { + return relatedModel.primaryKey + } + + return model.primaryKey + } + + /** + * The foreign key for a given model relationship + */ + relationForeignKey( + relation: ModelRelations['__opaque_type'], + model: LucidModel, + relatedModel: LucidModel + ): string { + if (relation === 'belongsTo') { + return string.camelCase(`${relatedModel.name}_${relatedModel.primaryKey}`) + } + + return string.camelCase(`${model.name}_${model.primaryKey}`) + } + + /** + * Pivot table name for many to many relationship + */ + relationPivotTable(_: 'manyToMany', model: LucidModel, relatedModel: LucidModel): string { + return string.snakeCase([relatedModel.name, model.name].sort().join('_')) + } + + /** + * Pivot foreign key for many to many relationship + */ + relationPivotForeignKey(_: 'manyToMany', model: LucidModel): string { + return string.snakeCase(`${model.name}_${model.primaryKey}`) + } + + /** + * Keys for the pagination meta + */ + paginationMetaKeys(): { + total: string + perPage: string + currentPage: string + lastPage: string + firstPage: string + firstPageUrl: string + lastPageUrl: string + nextPageUrl: string + previousPageUrl: string + } { + return { + total: 'total', + perPage: 'perPage', + currentPage: 'currentPage', + lastPage: 'lastPage', + firstPage: 'firstPage', + firstPageUrl: 'firstPageUrl', + lastPageUrl: 'lastPageUrl', + nextPageUrl: 'nextPageUrl', + previousPageUrl: 'previousPageUrl', + } + } +} diff --git a/test/orm/base_model.spec.ts b/test/orm/base_model.spec.ts index dc07d465..205c4b1b 100644 --- a/test/orm/base_model.spec.ts +++ b/test/orm/base_model.spec.ts @@ -249,7 +249,7 @@ test.group('Base model | boot', (group) => { } User.boot() - assert.deepEqual(User.$keys.attributesToSerialized.get('userName'), 'user_name') + assert.deepEqual(User.$keys.attributesToSerialized.get('userName'), 'userName') }) test('resolve attribute name from column name', async ({ fs, assert }) => { @@ -289,7 +289,7 @@ test.group('Base model | boot', (group) => { } User.boot() - assert.deepEqual(User.$keys.columnsToSerialized.get('user_name'), 'user_name') + assert.deepEqual(User.$keys.columnsToSerialized.get('user_name'), 'userName') }) test('resolve attribute name from serializeAs name', async ({ fs, assert }) => { @@ -309,7 +309,7 @@ test.group('Base model | boot', (group) => { } User.boot() - assert.deepEqual(User.$keys.serializedToAttributes.get('user_name'), 'userName') + assert.deepEqual(User.$keys.serializedToAttributes.get('userName'), 'userName') }) test('resolve column name from serializeAs name', async ({ fs, assert }) => { @@ -329,7 +329,7 @@ test.group('Base model | boot', (group) => { } User.boot() - assert.deepEqual(User.$keys.serializedToColumns.get('user_name'), 'user_name') + assert.deepEqual(User.$keys.serializedToColumns.get('userName'), 'user_name') }) }) @@ -2113,7 +2113,7 @@ test.group('Base Model | serializeRelations', () => { assert.deepEqual(user.serializeRelations(), { profile: { username: 'virk', - user_id: 1, + userId: 1, }, }) }) @@ -2150,7 +2150,7 @@ test.group('Base Model | serializeRelations', () => { assert.deepEqual(user.serializeRelations(), { userProfile: { username: 'virk', - user_id: 1, + userId: 1, }, }) }) @@ -2322,12 +2322,12 @@ test.group('Base Model | serializeRelations', () => { assert.deepEqual( user.serializeRelations({ profile: { - fields: ['user_id'], + fields: ['userId'], }, }), { profile: { - user_id: 1, + userId: 1, }, } ) @@ -2371,7 +2371,7 @@ test.group('Base Model | serializeRelations', () => { }), { profile: { - user_id: 1, + userId: 1, username: 'virk', }, } @@ -2506,7 +2506,7 @@ test.group('Base Model | toJSON', (group) => { const user = new User() user.username = 'virk' - assert.deepEqual(user.toJSON(), { username: 'virk', full_name: 'VIRK' }) + assert.deepEqual(user.toJSON(), { username: 'virk', fullName: 'VIRK' }) }) test('do not add computed property when it returns undefined', async ({ fs, assert }) => { @@ -2585,7 +2585,7 @@ test.group('Base Model | toJSON', (group) => { assert.deepEqual(user.toJSON(), { username: 'virk', - full_name: 'VIRK', + fullName: 'VIRK', meta: { postsCount: 10, }, @@ -2623,7 +2623,7 @@ test.group('Base Model | toJSON', (group) => { assert.deepEqual(user.toJSON(), { username: 'virk', - full_name: 'VIRK', + fullName: 'VIRK', posts: { count: 10, }, @@ -6191,7 +6191,7 @@ test.group('Base Model | date', (group) => { created_at: DateTime.local().toISODate(), }) const user = await User.find(1) - assert.match(user!.toJSON().created_at, /\d{4}-\d{2}-\d{2}/) + assert.match(user!.toJSON().createdAt, /\d{4}-\d{2}-\d{2}/) }) test('do not attempt to serialize, when already a string', async ({ fs, assert }) => { @@ -6223,7 +6223,7 @@ test.group('Base Model | date', (group) => { created_at: DateTime.local().toISODate(), }) const user = await User.find(1) - assert.equal(user!.toJSON().created_at, DateTime.local().minus({ days: 1 }).toISODate()) + assert.equal(user!.toJSON().createdAt, DateTime.local().minus({ days: 1 }).toISODate()) }) }) @@ -6667,7 +6667,7 @@ test.group('Base Model | datetime', (group) => { const user = await User.find(1) assert.match( - user!.toJSON().joined_at, + user!.toJSON().joinedAt, /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}(\+|\-)\d{2}:\d{2}/ ) }) @@ -6705,7 +6705,7 @@ test.group('Base Model | datetime', (group) => { }) const user = await User.find(1) - assert.equal(user!.toJSON().joined_at, DateTime.local().minus({ days: 1 }).toISODate()) + assert.equal(user!.toJSON().joinedAt, DateTime.local().minus({ days: 1 }).toISODate()) }) test('force update when enabledForceUpdate method is called', async ({ fs, assert }) => { @@ -6822,14 +6822,14 @@ test.group('Base Model | paginate', (group) => { assert.isTrue(users.hasTotal) assert.deepEqual(users.getMeta(), { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: '/users?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: '/users?page=2', + previousPageUrl: null, }) }) @@ -6867,14 +6867,14 @@ test.group('Base Model | paginate', (group) => { }) assert.deepEqual(meta, { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: '/users?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: '/users?page=2', + previousPageUrl: null, }) }) @@ -6915,14 +6915,14 @@ test.group('Base Model | paginate', (group) => { assert.isTrue(users.hasTotal) assert.deepEqual(users.getMeta(), { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: '/users?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: '/users?page=2', + previousPageUrl: null, }) }) @@ -7030,14 +7030,14 @@ test.group('Base Model | paginate', (group) => { assert.isTrue(users.hasTotal) assert.deepEqual(users.getMeta(), { total: 1, - per_page: 5, - current_page: 1, - last_page: 1, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=1', - next_page_url: null, - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 1, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=1', + nextPageUrl: null, + previousPageUrl: null, }) }) }) @@ -7493,7 +7493,7 @@ test.group('Base model | inheritance', (group) => { meta: undefined, prepare: undefined, serialize: undefined, - serializeAs: 'user_id', + serializeAs: 'userId', }, ], [ @@ -7533,7 +7533,7 @@ test.group('Base model | inheritance', (group) => { meta: undefined, prepare: undefined, serialize: undefined, - serializeAs: 'user_id', + serializeAs: 'userId', }, ], ]) @@ -7612,7 +7612,7 @@ test.group('Base model | inheritance', (group) => { 'fullName', { meta: undefined, - serializeAs: 'full_name', + serializeAs: 'fullName', }, ], [ @@ -7638,7 +7638,7 @@ test.group('Base model | inheritance', (group) => { 'fullName', { meta: undefined, - serializeAs: 'full_name', + serializeAs: 'fullName', }, ], ]) @@ -7733,7 +7733,7 @@ test.group('Base model | inheritance', (group) => { 'fullName', { meta: undefined, - serializeAs: 'full_name', + serializeAs: 'fullName', }, ], ]) diff --git a/test/orm/model_has_many.spec.ts b/test/orm/model_has_many.spec.ts index 39eb8606..2a5bd023 100644 --- a/test/orm/model_has_many.spec.ts +++ b/test/orm/model_has_many.spec.ts @@ -5291,14 +5291,14 @@ test.group('Model | HasMany | paginate', (group) => { assert.isTrue(posts.hasTotal) assert.deepEqual(posts.getMeta(), { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/posts?page=1', - last_page_url: '/posts?page=4', - next_page_url: '/posts?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/posts?page=1', + lastPageUrl: '/posts?page=4', + nextPageUrl: '/posts?page=2', + previousPageUrl: null, }) }) diff --git a/test/orm/model_has_many_through.spec.ts b/test/orm/model_has_many_through.spec.ts index bbf8f979..0f6841a8 100644 --- a/test/orm/model_has_many_through.spec.ts +++ b/test/orm/model_has_many_through.spec.ts @@ -3112,14 +3112,14 @@ test.group('Model | Has Many Through | pagination', (group) => { assert.isTrue(posts.hasTotal) assert.deepEqual(posts.getMeta(), { total: 3, - per_page: 2, - current_page: 1, - last_page: 2, - first_page: 1, - first_page_url: '/posts?page=1', - last_page_url: '/posts?page=2', - next_page_url: '/posts?page=2', - previous_page_url: null, + perPage: 2, + currentPage: 1, + lastPage: 2, + firstPage: 1, + firstPageUrl: '/posts?page=1', + lastPageUrl: '/posts?page=2', + nextPageUrl: '/posts?page=2', + previousPageUrl: null, }) }) diff --git a/test/orm/model_many_to_many.spec.ts b/test/orm/model_many_to_many.spec.ts index 00f27565..bb26c522 100644 --- a/test/orm/model_many_to_many.spec.ts +++ b/test/orm/model_many_to_many.spec.ts @@ -7385,14 +7385,14 @@ test.group('Model | ManyToMany | pagination', (group) => { assert.isTrue(skills.hasTotal) assert.deepEqual(skills.getMeta(), { total: 2, - per_page: 1, - current_page: 1, - last_page: 2, - first_page: 1, - first_page_url: '/skills?page=1', - last_page_url: '/skills?page=2', - next_page_url: '/skills?page=2', - previous_page_url: null, + perPage: 1, + currentPage: 1, + lastPage: 2, + firstPage: 1, + firstPageUrl: '/skills?page=1', + lastPageUrl: '/skills?page=2', + nextPageUrl: '/skills?page=2', + previousPageUrl: null, }) }) From d3278711805e7f4fc0809aea6f7829dc3893651a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 23 Jan 2024 11:32:43 +0530 Subject: [PATCH 06/73] fix: remove unused imports --- src/orm/base_model/index.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index 189d2ebb..1efbce2d 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -46,14 +46,17 @@ import { ManyToManyRelationOptions, } from '../../types/relations.js' -import { ModelKeys } from '../model_keys/index.js' +import * as errors from '../../errors.js' import { Preloader } from '../preloader/index.js' -import { HasOne } from '../relations/has_one/index.js' import { proxyHandler } from './proxy_handler.js' +import { ModelKeys } from '../model_keys/index.js' +import { HasOne } from '../relations/has_one/index.js' import { HasMany } from '../relations/has_many/index.js' import { BelongsTo } from '../relations/belongs_to/index.js' import { ManyToMany } from '../relations/many_to_many/index.js' import { HasManyThrough } from '../relations/has_many_through/index.js' +import { CamelCaseNamingStrategy } from '../naming_strategies/camel_case.js' +import { LazyLoadAggregates } from '../relations/aggregates_loader/lazy_load.js' import { isObject, collectValues, @@ -61,10 +64,6 @@ import { managedTransaction, normalizeCherryPickObject, } from '../../utils/index.js' -import { SnakeCaseNamingStrategy } from '../naming_strategies/snake_case.js' -import { LazyLoadAggregates } from '../relations/aggregates_loader/lazy_load.js' -import * as errors from '../../errors.js' -import { CamelCaseNamingStrategy } from '../naming_strategies/camel_case.js' const MANY_RELATIONS = ['hasMany', 'manyToMany', 'hasManyThrough'] const DATE_TIME_TYPES = { From 7e35cc26aaa12251d8376271d1bbb73c19181e85 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 23 Jan 2024 13:55:35 +0530 Subject: [PATCH 07/73] refactor: use camelCase naming strategy with paginator also --- src/database/paginator/simple_paginator.ts | 4 +- test/database/query_builder.spec.ts | 112 ++++++++++----------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/database/paginator/simple_paginator.ts b/src/database/paginator/simple_paginator.ts index d9cae8b8..a5709bd9 100644 --- a/src/database/paginator/simple_paginator.ts +++ b/src/database/paginator/simple_paginator.ts @@ -8,8 +8,8 @@ */ import { stringify } from 'qs' +import { CamelCaseNamingStrategy } from '../../orm/naming_strategies/camel_case.js' import { SimplePaginatorContract, SimplePaginatorMetaKeys } from '../../types/querybuilder.js' -import { SnakeCaseNamingStrategy } from '../../orm/naming_strategies/snake_case.js' /** * Simple paginator works with the data set provided by the standard @@ -25,7 +25,7 @@ export class SimplePaginator extends Array implements SimplePaginatorContract { assert.deepEqual(users.getMeta(), { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: '/users?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: '/users?page=2', + previousPageUrl: null, }) await connection.disconnect() @@ -10347,14 +10347,14 @@ test.group('Query Builder | paginate', (group) => { assert.isTrue(users.hasTotal) assert.deepEqual(users.getMeta(), { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: '/users?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: '/users?page=2', + previousPageUrl: null, }) await connection.disconnect() @@ -10381,14 +10381,14 @@ test.group('Query Builder | paginate', (group) => { assert.isTrue(users.hasTotal) assert.deepEqual(users.getMeta(), { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: '/users?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: '/users?page=2', + previousPageUrl: null, }) await connection.disconnect() @@ -10416,14 +10416,14 @@ test.group('Query Builder | paginate', (group) => { assert.deepEqual(users.getMeta(), { total: 18, - per_page: 5, - current_page: 4, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: null, - previous_page_url: '/users?page=3', + perPage: 5, + currentPage: 4, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: null, + previousPageUrl: '/users?page=3', }) await connection.disconnect() @@ -10456,14 +10456,14 @@ test.group('Query Builder | paginate', (group) => { assert.isTrue(users.hasTotal) assert.deepEqual(users.getMeta(), { total: 18, - per_page: 5, - current_page: 1, - last_page: 4, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=4', - next_page_url: '/users?page=2', - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 4, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=4', + nextPageUrl: '/users?page=2', + previousPageUrl: null, }) await connection.disconnect() @@ -10498,14 +10498,14 @@ test.group('Query Builder | paginate', (group) => { assert.isTrue(results.hasTotal) assert.deepEqual(results.getMeta(), { total: 2, - per_page: 1, - current_page: 1, - last_page: 2, - first_page: 1, - first_page_url: '/users-country-ids?page=1', - last_page_url: '/users-country-ids?page=2', - next_page_url: '/users-country-ids?page=2', - previous_page_url: null, + perPage: 1, + currentPage: 1, + lastPage: 2, + firstPage: 1, + firstPageUrl: '/users-country-ids?page=1', + lastPageUrl: '/users-country-ids?page=2', + nextPageUrl: '/users-country-ids?page=2', + previousPageUrl: null, }) await connection.disconnect() @@ -10640,14 +10640,14 @@ test.group('Query Builder | paginate', (group) => { assert.deepEqual(users.getMeta(), { total: 1, - per_page: 5, - current_page: 1, - last_page: 1, - first_page: 1, - first_page_url: '/users?page=1', - last_page_url: '/users?page=1', - next_page_url: null, - previous_page_url: null, + perPage: 5, + currentPage: 1, + lastPage: 1, + firstPage: 1, + firstPageUrl: '/users?page=1', + lastPageUrl: '/users?page=1', + nextPageUrl: null, + previousPageUrl: null, }) await connection.disconnect() From cee062c59608267fa7b562dbcfa7f65d63247b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Tue, 23 Jan 2024 15:14:13 +0100 Subject: [PATCH 08/73] chore: remove `@types/pluralize` from dependencies It's not used directly in the source code. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 86f1dc14..6c0159fb 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", "@types/node": "^20.11.5", - "@types/pluralize": "^0.0.33", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.11", "@vinejs/vine": "^1.7.0", From 4777edc087ea99bb3177757595e353f8604332d2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jan 2024 09:52:37 +0530 Subject: [PATCH 09/73] chore(package): update dependencies --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6c0159fb..efb81fa9 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "index:commands": "adonis-kit index build/commands" }, "dependencies": { - "@adonisjs/presets": "^2.2.1", + "@adonisjs/presets": "^2.2.3", "@faker-js/faker": "^8.3.1", "@poppinss/hooks": "^7.2.2", "@poppinss/macroable": "^1.0.1", @@ -84,7 +84,7 @@ "@swc/core": "^1.3.105", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.11.5", + "@types/node": "^20.11.6", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.11", "@vinejs/vine": "^1.7.0", @@ -94,13 +94,13 @@ "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "dotenv": "^16.3.2", + "dotenv": "^16.4.1", "eslint": "^8.56.0", "fs-extra": "^11.2.0", "github-label-sync": "^2.3.1", - "husky": "^8.0.3", + "husky": "^9.0.1", "luxon": "^3.4.4", - "mysql2": "^3.7.1", + "mysql2": "^3.8.0", "np": "^9.2.0", "pg": "^8.11.0", "prettier": "^3.2.4", From b4af59304c4d2c3d8e225dc17724ead704512fdf Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jan 2024 09:58:13 +0530 Subject: [PATCH 10/73] feat: add support for nulls property for orderby method signature --- src/types/querybuilder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/querybuilder.ts b/src/types/querybuilder.ts index 66ffe911..dd23c2cb 100644 --- a/src/types/querybuilder.ts +++ b/src/types/querybuilder.ts @@ -343,6 +343,7 @@ export interface OrderBy { columns: { column: string | ChainableContract | RawBuilderContract | RawQuery order?: 'asc' | 'desc' + nulls?: 'first' | 'last' }[] ): Builder } From 42b46f3120b6339128b422c2adc1fc503a394b9c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jan 2024 10:02:45 +0530 Subject: [PATCH 11/73] chore(release): 20.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efb81fa9..68ecd39c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "19.0.0", + "version": "20.0.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From 629ca327df6b00a3afddc9155169058b6693b50d Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Sun, 28 Jan 2024 15:23:22 +0100 Subject: [PATCH 12/73] feat: add @adonisjs/lucid/migration submodule Close #993 --- package.json | 1 + src/migration/main.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 src/migration/main.ts diff --git a/package.json b/package.json index 68ecd39c..5bfeef2a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "./seeders": "./build/src/seeders/main.js", "./services/*": "./build/services/*.js", "./types/*": "./build/src/types/*.js", + "./migration": "./build/src/migration/main.js", "./database_provider": "./build/providers/database_provider.js" }, "scripts": { diff --git a/src/migration/main.ts b/src/migration/main.ts new file mode 100644 index 00000000..bf94e34e --- /dev/null +++ b/src/migration/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { MigrationRunner } from './runner.js' From 9c845f62a0c0bf40f48089cce94f4df40daa86c0 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Mon, 29 Jan 2024 12:51:00 +0100 Subject: [PATCH 13/73] Add DatabaseTestUtils (#988) * chore: add a quick:test script * refactor: rename Rollback class * feat: add DatabaseTestUtils class * feat(wip): update database provider to register testUtils * refactor: use test_utils import path * chore: update @adonisjs/core * refactor: use resolving for extending testUtils * refactor: resolve ace only when one of testUtils.db() method is called * test: check if testUtils.db is registered * refactor: remove unused ts-expect-error --- commands/migration/rollback.ts | 2 +- package.json | 5 +- providers/database_provider.ts | 21 +++ src/test_utils/database.ts | 78 ++++++++++ test/database_provider.spec.ts | 35 +++++ test/test_utils.spec.ts | 259 +++++++++++++++++++++++++++++++++ 6 files changed, 397 insertions(+), 3 deletions(-) create mode 100644 src/test_utils/database.ts create mode 100644 test/test_utils.spec.ts diff --git a/commands/migration/rollback.ts b/commands/migration/rollback.ts index 56ba5f30..592baee6 100644 --- a/commands/migration/rollback.ts +++ b/commands/migration/rollback.ts @@ -17,7 +17,7 @@ import { CommandOptions } from '@adonisjs/core/types/ace' * The command is meant to migrate the database by executing migrations * in `down` direction. */ -export default class Migrate extends MigrationsBase { +export default class Rollback extends MigrationsBase { static commandName = 'migration:rollback' static description = 'Rollback migrations to a specific batch number' static options: CommandOptions = { diff --git a/package.json b/package.json index 5bfeef2a..98199861 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "test:mssql": "DB=mssql node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "test:pg": "DB=pg node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "test:docker": "npm run test:mysql && npm run test:mysql_legacy && npm run test:pg && npm run test:mssql", + "quick:test": "DB=sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "lint": "eslint . --ext=.ts", "clean": "del-cli build", "compile": "npm run lint && npm run clean && tsc", @@ -73,7 +74,7 @@ }, "devDependencies": { "@adonisjs/assembler": "^7.1.0", - "@adonisjs/core": "^6.2.1", + "@adonisjs/core": "^6.2.2", "@adonisjs/eslint-config": "^1.2.1", "@adonisjs/prettier-config": "^1.2.1", "@adonisjs/tsconfig": "^1.2.1", @@ -113,7 +114,7 @@ }, "peerDependencies": { "@adonisjs/assembler": "^7.0.0", - "@adonisjs/core": "^6.2.0", + "@adonisjs/core": "^6.2.2", "luxon": "^3.4.4" }, "peerDependenciesMeta": { diff --git a/providers/database_provider.ts b/providers/database_provider.ts index 5f4c40e4..3940d0db 100644 --- a/providers/database_provider.ts +++ b/providers/database_provider.ts @@ -14,6 +14,7 @@ import { Database } from '../src/database/main.js' import { Adapter } from '../src/orm/adapter/index.js' import { QueryClient } from '../src/query_client/index.js' import { BaseModel } from '../src/orm/base_model/index.js' +import { DatabaseTestUtils } from '../src/test_utils/database.js' import type { DatabaseConfig, DbQueryEventNode } from '../src/types/database.js' /** @@ -28,6 +29,12 @@ declare module '@adonisjs/core/types' { } } +declare module '@adonisjs/core/test_utils' { + export interface TestUtils { + db(connectionName?: string): DatabaseTestUtils + } +} + /** * Extending VineJS schema types */ @@ -80,6 +87,19 @@ export default class DatabaseServiceProvider { } } + /** + * Register TestUtils database macro + */ + protected async registerTestUtils() { + this.app.container.resolving('testUtils', async () => { + const { TestUtils } = await import('@adonisjs/core/test_utils') + + TestUtils.macro('db', (connectionName?: string) => { + return new DatabaseTestUtils(this.app, connectionName) + }) + }) + } + /** * Invoked by AdonisJS to register container bindings */ @@ -107,6 +127,7 @@ export default class DatabaseServiceProvider { const db = await this.app.container.make('lucid.db') BaseModel.$adapter = new Adapter(db) + await this.registerTestUtils() await this.registerReplBindings() await this.registerVineJSRules(db) } diff --git a/src/test_utils/database.ts b/src/test_utils/database.ts new file mode 100644 index 00000000..07e5a88d --- /dev/null +++ b/src/test_utils/database.ts @@ -0,0 +1,78 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ApplicationService } from '@adonisjs/core/types' + +/** + * Database test utils are meant to be used during testing to + * perform common tasks like running migrations, seeds, etc. + */ +export class DatabaseTestUtils { + constructor( + protected app: ApplicationService, + protected connectionName?: string + ) {} + + /** + * Runs a command through Ace + */ + async #runCommand(commandName: string, args: string[] = []) { + if (this.connectionName) { + args.push(`--connection=${this.connectionName}`) + } + + const ace = await this.app.container.make('ace') + const command = await ace.exec(commandName, args) + if (!command.exitCode) return + + if (command.error) { + throw command.error + } else { + throw new Error(`"${commandName}" failed`) + } + } + + /** + * Testing hook for running migrations ( if needed ) + * Return a function to truncate the whole database but keep the schema + */ + async truncate() { + await this.#runCommand('migration:run', ['--compact-output']) + return () => this.#runCommand('db:truncate') + } + + /** + * Testing hook for running seeds + */ + async seed() { + await this.#runCommand('db:seed') + } + + /** + * Testing hook for running migrations + * Return a function to rollback the whole database + * + * Note that this is slower than truncate() because it + * has to run all migration in both directions when running tests + */ + async migrate() { + await this.#runCommand('migration:run', ['--compact-output']) + return () => this.#runCommand('migration:rollback', ['--compact-output']) + } + + /** + * Testing hook for creating a global transaction + */ + async withGlobalTransaction() { + const db = await this.app.container.make('lucid.db') + + await db.beginGlobalTransaction(this.connectionName) + return () => db.rollbackGlobalTransaction(this.connectionName) + } +} diff --git a/test/database_provider.spec.ts b/test/database_provider.spec.ts index 85300dee..6c254687 100644 --- a/test/database_provider.spec.ts +++ b/test/database_provider.spec.ts @@ -81,4 +81,39 @@ test.group('Database Provider', () => { assert.isFalse(db.manager.isConnected('sqlite')) }) + + test('register testUtils.db() binding', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [() => import('../providers/database_provider.js')], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + database: defineConfig({ + connection: 'sqlite', + connections: { + sqlite: { + client: 'sqlite', + connection: { filename: new URL('./tmp/database.sqlite', import.meta.url).href }, + migrations: { naturalSort: true, paths: ['database/migrations'] }, + }, + }, + }), + }, + }) + .create(BASE_URL, { importer: IMPORTER }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + const testUtils = await app.container.make('testUtils') + + assert.isDefined(testUtils.db) + assert.isFunction(testUtils.db) + }) }) diff --git a/test/test_utils.spec.ts b/test/test_utils.spec.ts new file mode 100644 index 00000000..0390240e --- /dev/null +++ b/test/test_utils.spec.ts @@ -0,0 +1,259 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { ListLoader } from '@adonisjs/core/ace' +import { AceFactory } from '@adonisjs/core/factories' + +import DbSeed from '../commands/db_seed.js' +import { getDb } from '../test-helpers/index.js' +import Migrate from '../commands/migration/run.js' +import DbTruncate from '../commands/db_truncate.js' +import Rollback from '../commands/migration/rollback.js' +import { AppFactory } from '@adonisjs/core/factories/app' +import { ApplicationService } from '@adonisjs/core/types' +import { DatabaseTestUtils } from '../src/test_utils/database.js' + +test.group('Database Test Utils', () => { + test('truncate() should run migration:run and db:truncate commands', async ({ fs, assert }) => { + let migrationRun = false + let truncateRun = false + + class FakeMigrate extends Migrate { + override async run() { + migrationRun = true + } + } + + class FakeDbTruncate extends DbTruncate { + override async run() { + truncateRun = true + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeMigrate, FakeDbTruncate])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app as ApplicationService) + const truncate = await dbTestUtils.truncate() + + await truncate() + + assert.isTrue(migrationRun) + assert.isTrue(truncateRun) + }) + + test('truncate() with custom connectionName', async ({ fs, assert }) => { + assert.plan(2) + + class FakeMigrate extends Migrate { + override async run() { + assert.equal(this.connection, 'secondary') + } + } + + class FakeDbTruncate extends DbTruncate { + override async run() { + assert.equal(this.connection, 'secondary') + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeMigrate, FakeDbTruncate])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app, 'secondary') + const truncate = await dbTestUtils.truncate() + + await truncate() + }) + + test('seed() should run db:seed command', async ({ fs, assert }) => { + assert.plan(1) + + class FakeDbSeed extends DbSeed { + override async run() { + assert.isTrue(true) + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeDbSeed])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app) + await dbTestUtils.seed() + }) + + test('seed() with custom connectionName', async ({ fs, assert }) => { + assert.plan(1) + + class FakeDbSeed extends DbSeed { + override async run() { + assert.equal(this.connection, 'secondary') + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeDbSeed])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app, 'secondary') + await dbTestUtils.seed() + }) + + test('migrate() should run migration:run and migration:rollback commands', async ({ + fs, + assert, + }) => { + let migrationRun = false + let rollbackRun = false + + class FakeMigrate extends Migrate { + override async run() { + migrationRun = true + } + } + + class FakeMigrationRollback extends Rollback { + override async run() { + rollbackRun = true + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeMigrate, FakeMigrationRollback])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app) + const rollback = await dbTestUtils.migrate() + + await rollback() + + assert.isTrue(migrationRun) + assert.isTrue(rollbackRun) + }) + + test('migrate() with custom connectionName', async ({ fs, assert }) => { + assert.plan(2) + + class FakeMigrate extends Migrate { + override async run() { + assert.equal(this.connection, 'secondary') + } + } + + class FakeMigrationRollback extends Rollback { + override async run() { + assert.equal(this.connection, 'secondary') + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeMigrate, FakeMigrationRollback])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app, 'secondary') + const rollback = await dbTestUtils.migrate() + + await rollback() + }) + + test('should throw error when command has an exitCode = 1', async ({ fs }) => { + class FakeMigrate extends Migrate { + override async run() { + this.exitCode = 1 + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeMigrate])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app) + await dbTestUtils.migrate() + }).throws('"migration:run" failed') + + test('should re-use command.error message if available', async ({ fs }) => { + class FakeMigrate extends Migrate { + override async run() { + this.exitCode = 1 + this.error = new Error('Custom error message') + } + } + + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + ace.addLoader(new ListLoader([FakeMigrate])) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => getDb()) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app) + await dbTestUtils.migrate() + }).throws('Custom error message') + + test('withGlobalTransaction should wrap and rollback a transaction', async ({ fs, assert }) => { + const db = getDb() + const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} }) + + const app = new AppFactory().create(fs.baseUrl, () => {}) as ApplicationService + await app.init() + + app.container.bind('lucid.db', () => db) + app.container.bind('ace', () => ace) + + const dbTestUtils = new DatabaseTestUtils(app) + const rollback = await dbTestUtils.withGlobalTransaction() + + assert.isDefined(db.connectionGlobalTransactions.get(db.primaryConnectionName)) + + await rollback() + + assert.isUndefined(db.connectionGlobalTransactions.get(db.primaryConnectionName)) + }) +}) From c487b1a10d96572575384c91f2fe59ce58535f4a Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Mon, 29 Jan 2024 13:00:19 +0100 Subject: [PATCH 14/73] chore(release): 20.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98199861..de87a953 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.0.0", + "version": "20.1.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From cb78f1a0b51206eea5d62208afc25b8893f55626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C3=ABl=20Guilloux?= Date: Mon, 5 Feb 2024 23:56:44 +0100 Subject: [PATCH 15/73] fix(vinejs): add `exists` and `unique` bindings to `VineNumber` string as well --- providers/database_provider.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/providers/database_provider.ts b/providers/database_provider.ts index 3940d0db..9596c55d 100644 --- a/providers/database_provider.ts +++ b/providers/database_provider.ts @@ -39,7 +39,7 @@ declare module '@adonisjs/core/test_utils' { * Extending VineJS schema types */ declare module '@vinejs/vine' { - export interface VineString { + interface VineLucidBindings { /** * Ensure the value is unique inside the database by self * executing a query. @@ -58,6 +58,10 @@ declare module '@vinejs/vine' { */ exists(callback: (db: Database, value: string, field: FieldContext) => Promise): this } + + interface VineNumber extends VineLucidBindings {} + + interface VineString extends VineLucidBindings {} } /** From 02c153f222e76d24aa782c6e08e3ab60d571e2bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C3=ABl=20Guilloux?= Date: Tue, 6 Feb 2024 00:22:03 +0100 Subject: [PATCH 16/73] feat(vinejs): add binding on VineNumber in `vinejs.ts` --- src/bindings/vinejs.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/bindings/vinejs.ts b/src/bindings/vinejs.ts index 1e2d0612..1db97322 100644 --- a/src/bindings/vinejs.ts +++ b/src/bindings/vinejs.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import vine, { VineString } from '@vinejs/vine' +import vine, { VineNumber, VineString } from '@vinejs/vine' import type { Database } from '../database/main.js' /** @@ -15,7 +15,7 @@ import type { Database } from '../database/main.js' * VineJS. */ export function defineValidationRules(db: Database) { - const uniqueRule = vine.createRule[0]>( + const uniqueRule = vine.createRule[0]>( async (value, checker, field) => { if (!field.isValid) { return @@ -28,7 +28,7 @@ export function defineValidationRules(db: Database) { } ) - const existsRule = vine.createRule[0]>( + const existsRule = vine.createRule[0]>( async (value, checker, field) => { if (!field.isValid) { return @@ -47,4 +47,10 @@ export function defineValidationRules(db: Database) { VineString.macro('exists', function (this: VineString, checker) { return this.use(existsRule(checker)) }) + VineNumber.macro('unique', function (this: VineNumber, checker) { + return this.use(uniqueRule(checker)) + }) + VineNumber.macro('exists', function (this: VineNumber, checker) { + return this.use(existsRule(checker)) + }) } From 5ec314bc75720cd3111e7e375c7c596bee830124 Mon Sep 17 00:00:00 2001 From: Hamidi_pour <87132724+amir-mohammad-HP@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:43:03 +0330 Subject: [PATCH 17/73] Update database_provider.ts (#1002) fix comment on exists method --- providers/database_provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/database_provider.ts b/providers/database_provider.ts index 9596c55d..3c8164c2 100644 --- a/providers/database_provider.ts +++ b/providers/database_provider.ts @@ -53,8 +53,8 @@ declare module '@vinejs/vine' { * Ensure the value is exists inside the database by self * executing a query. * - * - The callback must return "true", if the value exists. - * - The callback must return "false", if the value does not exist. + * - The callback must return "false", if the value exists. + * - The callback must return "true", if the value does not exist. */ exists(callback: (db: Database, value: string, field: FieldContext) => Promise): this } From 659b061f7abf99ec374b112d01f5f428463551a4 Mon Sep 17 00:00:00 2001 From: Maxime <57860498+MaximeMRF@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:48:18 +0700 Subject: [PATCH 18/73] feat: drop postgres domain (#981) * feat: drop postgres domain * fix(TransactionClient): adding missing methods * fix: adapt for v6 --- commands/db_wipe.ts | 23 +++++++++++++++++ commands/migration/fresh.ts | 10 ++++++++ src/dialects/base_sqlite.ts | 15 +++++++++++ src/dialects/mssql.ts | 13 ++++++++++ src/dialects/mysql.ts | 15 +++++++++++ src/dialects/oracle.ts | 13 ++++++++++ src/dialects/pg.ts | 37 ++++++++++++++++++++++++++++ src/dialects/red_shift.ts | 41 +++++++++++++++++++++++++++++++ src/query_client/index.ts | 14 +++++++++++ src/transaction_client/index.ts | 14 +++++++++++ src/types/database.ts | 14 +++++++++++ test/database/views_types.spec.ts | 29 +++++++++++++++++++++- 12 files changed, 237 insertions(+), 1 deletion(-) diff --git a/commands/db_wipe.ts b/commands/db_wipe.ts index 43c59268..aab3cc4a 100644 --- a/commands/db_wipe.ts +++ b/commands/db_wipe.ts @@ -37,6 +37,12 @@ export default class DbWipe extends BaseCommand { @flags.boolean({ description: 'Drop all custom types (Postgres only)' }) declare dropTypes: boolean + /** + * Drop all domains in database + */ + @flags.boolean({ description: 'Drop all domains (Postgres only)' }) + declare dropDomains: boolean + /** * Force command execution in production */ @@ -104,6 +110,22 @@ export default class DbWipe extends BaseCommand { this.logger.success('Dropped types successfully') } + /** + * Drop all domains (if asked for and supported) + */ + private async performDropDomains(client: QueryClientContract, schemas: string[]) { + if (!this.dropDomains) { + return + } + + if (!client.dialect.supportsDomains) { + this.logger.warning(`Dropping domains is not supported by "${client.dialect.name}"`) + } + + await client.dropAllDomains(schemas) + this.logger.success('Dropped domains successfully') + } + /** * Run as a subcommand. Never close database connections or exit * process inside this method @@ -147,6 +169,7 @@ export default class DbWipe extends BaseCommand { await this.performDropViews(connection, schemas) await this.performDropTables(connection, schemas) await this.performDropTypes(connection, schemas) + await this.performDropDomains(connection, schemas) } /** diff --git a/commands/migration/fresh.ts b/commands/migration/fresh.ts index 97bc5771..9ee62356 100644 --- a/commands/migration/fresh.ts +++ b/commands/migration/fresh.ts @@ -51,6 +51,12 @@ export default class Refresh extends BaseCommand { @flags.boolean({ description: 'Drop all custom types (Postgres only)' }) declare dropTypes: boolean + /** + * Drop all domains in database + */ + @flags.boolean({ description: 'Drop all domains (Postgres only)' }) + declare dropDomains: boolean + /** * Disable advisory locks */ @@ -86,6 +92,10 @@ export default class Refresh extends BaseCommand { args.push('--drop-types') } + if (this.dropDomains) { + args.push('--drop-domains') + } + if (this.dropViews) { args.push('--drop-views') } diff --git a/src/dialects/base_sqlite.ts b/src/dialects/base_sqlite.ts index cdbd567c..d81271e4 100644 --- a/src/dialects/base_sqlite.ts +++ b/src/dialects/base_sqlite.ts @@ -14,6 +14,7 @@ export abstract class BaseSqliteDialect implements DialectContract { readonly supportsAdvisoryLocks = false readonly supportsViews = true readonly supportsTypes = false + readonly supportsDomains = false readonly supportsReturningStatement = false /** @@ -72,6 +73,13 @@ export abstract class BaseSqliteDialect implements DialectContract { throw new Error("Sqlite doesn't support types") } + /** + * Returns an array of all domains names + */ + async getAllDomains(): Promise { + throw new Error("Sqlite doesn't support domains") + } + /** * Truncate SQLITE tables */ @@ -112,6 +120,13 @@ export abstract class BaseSqliteDialect implements DialectContract { throw new Error("Sqlite doesn't support types") } + /** + * Drop all custom domains inside the database + */ + async dropAllDomains(): Promise { + throw new Error("Sqlite doesn't support domains") + } + /** * Attempts to add advisory lock to the database and * returns it's status. diff --git a/src/dialects/mssql.ts b/src/dialects/mssql.ts index f5588883..66ea6617 100644 --- a/src/dialects/mssql.ts +++ b/src/dialects/mssql.ts @@ -15,6 +15,7 @@ export class MssqlDialect implements DialectContract { readonly supportsAdvisoryLocks = false readonly supportsViews = false readonly supportsTypes = false + readonly supportsDomains = false readonly supportsReturningStatement = true /** @@ -99,6 +100,12 @@ export class MssqlDialect implements DialectContract { ) } + async getAllDomains(): Promise { + throw new Error( + '"getAllDomains" method not implemented is not implemented for mssql. Create a PR to add the feature' + ) + } + async dropAllViews(): Promise { throw new Error( '"dropAllViews" method not implemented is not implemented for mssql. Create a PR to add the feature' @@ -111,6 +118,12 @@ export class MssqlDialect implements DialectContract { ) } + async dropAllDomains(): Promise { + throw new Error( + '"dropAllDomains" method not implemented is not implemented for mssql. Create a PR to add the feature' + ) + } + getAdvisoryLock(): Promise { throw new Error( 'Support for advisory locks is not implemented for mssql. Create a PR to add the feature' diff --git a/src/dialects/mysql.ts b/src/dialects/mysql.ts index 72738609..5e93fca1 100644 --- a/src/dialects/mysql.ts +++ b/src/dialects/mysql.ts @@ -15,6 +15,7 @@ export class MysqlDialect implements DialectContract { readonly supportsAdvisoryLocks = true readonly supportsViews = true readonly supportsTypes = false + readonly supportsDomains = false readonly supportsReturningStatement = false /** @@ -96,6 +97,13 @@ export class MysqlDialect implements DialectContract { throw new Error("MySQL doesn't support types") } + /** + * Returns an array of all domain names + */ + async getAllDomains(): Promise { + throw new Error("MySQL doesn't support domains") + } + /** * Drop all tables inside the database */ @@ -149,6 +157,13 @@ export class MysqlDialect implements DialectContract { throw new Error("MySQL doesn't support types") } + /** + * Drop all domains inside the database + */ + async dropAllDomains(): Promise { + throw new Error("MySQL doesn't support domains") + } + /** * Attempts to add advisory lock to the database and * returns it's status. diff --git a/src/dialects/oracle.ts b/src/dialects/oracle.ts index 196e42c4..3bdc2eae 100644 --- a/src/dialects/oracle.ts +++ b/src/dialects/oracle.ts @@ -14,6 +14,7 @@ export class OracleDialect implements DialectContract { readonly supportsAdvisoryLocks = false readonly supportsViews = false readonly supportsTypes = false + readonly supportsDomains = false readonly supportsReturningStatement = true /** @@ -71,6 +72,12 @@ export class OracleDialect implements DialectContract { ) } + async getAllDomains(): Promise { + throw new Error( + '"getAllDomains" method is not implemented for oracledb. Create a PR to add the feature.' + ) + } + async dropAllViews(): Promise { throw new Error( '"dropAllViews" method is not implemented for oracledb. Create a PR to add the feature.' @@ -83,6 +90,12 @@ export class OracleDialect implements DialectContract { ) } + async dropAllDomains(): Promise { + throw new Error( + '"dropAllDomains" method is not implemented for oracledb. Create a PR to add the feature.' + ) + } + getAdvisoryLock(): Promise { throw new Error( 'Support for advisory locks is not implemented for oracledb. Create a PR to add the feature' diff --git a/src/dialects/pg.ts b/src/dialects/pg.ts index dde80f6e..6c58a13e 100644 --- a/src/dialects/pg.ts +++ b/src/dialects/pg.ts @@ -14,6 +14,7 @@ export class PgDialect implements DialectContract { readonly supportsAdvisoryLocks = true readonly supportsViews = true readonly supportsTypes = true + readonly supportsDomains = true readonly supportsReturningStatement = true /** @@ -77,6 +78,21 @@ export class PgDialect implements DialectContract { return types.map(({ typname }) => typname) } + /** + * Returns an array of all domain names + */ + async getAllDomains(_schemas: string[]) { + const domains = await this.client + .query() + .select('pg_type.typname') + .distinct() + .from('pg_type') + .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_type.typnamespace') + .where('pg_type.typtype', 'd') + + return domains.map(({ typname }) => typname) + } + /** * Truncate pg table with option to cascade and restart identity */ @@ -126,6 +142,27 @@ export class PgDialect implements DialectContract { await this.client.rawQuery(`DROP TYPE "${types.join('", "')}" CASCADE;`) } + /** + * Drop all domains inside the database + */ + async dropAllDomains(schemas: string[]) { + const domains = await this.getAllDomains(schemas) + if (!domains.length) return + + // Don't drop built-in domains + // https://www.postgresql.org/docs/current/infoschema-datatypes.html + const builtInDomains = [ + 'cardinal_number', + 'character_data', + 'sql_identifier', + 'time_stamp', + 'yes_or_no', + ] + const domainsToDrop = domains.filter((domain) => !builtInDomains.includes(domain)) + + await this.client.rawQuery(`DROP DOMAIN "${domainsToDrop.join('", "')}" CASCADE;`) + } + /** * Attempts to add advisory lock to the database and * returns it's status. diff --git a/src/dialects/red_shift.ts b/src/dialects/red_shift.ts index bd8ed2f2..80afbe50 100644 --- a/src/dialects/red_shift.ts +++ b/src/dialects/red_shift.ts @@ -14,6 +14,7 @@ export class RedshiftDialect implements DialectContract { readonly supportsAdvisoryLocks = false readonly supportsViews = true readonly supportsTypes = true + readonly supportsDomains = true readonly supportsReturningStatement = true /** @@ -83,6 +84,23 @@ export class RedshiftDialect implements DialectContract { return types.map(({ typname }) => typname) } + /** + * Returns an array of all domain names + * + * NOTE: ASSUMING FEATURE PARITY WITH POSTGRESQL HERE (NOT TESTED) + */ + async getAllDomains(_schemas: string[]) { + const domains = await this.client + .query() + .select('pg_type.typname') + .distinct() + .from('pg_type') + .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_type.typnamespace') + .where('pg_type.typtype', 'd') + + return domains.map(({ typname }) => typname) + } + /** * Truncate redshift table with option to cascade and restart identity. * @@ -138,6 +156,29 @@ export class RedshiftDialect implements DialectContract { await this.client.rawQuery(`DROP type ${types.join(',')};`) } + /** + * Drop all domains inside the database + * + * NOTE: ASSUMING FEATURE PARITY WITH POSTGRESQL HERE (NOT TESTED) + */ + async dropAllDomains(schemas: string[]) { + const domains = await this.getAllDomains(schemas) + if (!domains.length) return + + // Don't drop built-in domains + // https://www.postgresql.org/docs/current/infoschema-datatypes.html + const builtInDomains = [ + 'cardinal_number', + 'character_data', + 'sql_identifier', + 'time_stamp', + 'yes_or_no', + ] + const domainsToDrop = domains.filter((domain) => !builtInDomains.includes(domain)) + + await this.client.rawQuery(`DROP DOMAIN "${domainsToDrop.join('", "')}" CASCADE;`) + } + /** * Redshift doesn't support advisory locks. Learn more: * https://tableplus.com/blog/2018/10/redshift-vs-postgres-database-comparison.html diff --git a/src/query_client/index.ts b/src/query_client/index.ts index 1473f7a9..2cf5cd0f 100644 --- a/src/query_client/index.ts +++ b/src/query_client/index.ts @@ -160,6 +160,13 @@ export class QueryClient implements QueryClientContract { return this.dialect.getAllTypes(schemas) } + /** + * Returns an array of all domain names + */ + async getAllDomains(schemas?: string[]): Promise { + return this.dialect.getAllDomains(schemas) + } + /** * Drop all tables inside database */ @@ -181,6 +188,13 @@ export class QueryClient implements QueryClientContract { return this.dialect.dropAllTypes(schemas || ['public']) } + /** + * Drop all custom domains inside the database + */ + async dropAllDomains(schemas?: string[]): Promise { + return this.dialect.dropAllDomains(schemas || ['public']) + } + /** * Returns an instance of a transaction. Each transaction will * query and hold a single connection for all queries. diff --git a/src/transaction_client/index.ts b/src/transaction_client/index.ts index 81ae6965..c7368b6e 100644 --- a/src/transaction_client/index.ts +++ b/src/transaction_client/index.ts @@ -124,6 +124,13 @@ export class TransactionClient extends EventEmitter implements TransactionClient return this.dialect.getAllTypes(schemas) } + /** + * Returns an array of all domains names + */ + async getAllDomains(schemas?: string[]): Promise { + return this.dialect.getAllDomains(schemas) + } + /** * Drop all tables inside database */ @@ -145,6 +152,13 @@ export class TransactionClient extends EventEmitter implements TransactionClient return this.dialect.dropAllTypes(schemas || ['public']) } + /** + * Drop all domains inside the database + */ + async dropAllDomains(schemas?: string[]): Promise { + return this.dialect.dropAllDomains(schemas || ['public']) + } + /** * Get a new query builder instance */ diff --git a/src/types/database.ts b/src/types/database.ts index 1b57ef7f..4138d48d 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -62,6 +62,7 @@ export interface DialectContract { readonly supportsAdvisoryLocks: boolean readonly supportsViews: boolean readonly supportsTypes: boolean + readonly supportsDomains: boolean readonly supportsReturningStatement: boolean getAllTables(schemas?: string[]): Promise @@ -73,6 +74,9 @@ export interface DialectContract { getAllTypes(schemas?: string[]): Promise dropAllTypes(schemas?: string[]): Promise + getAllDomains(schemas?: string[]): Promise + dropAllDomains(schemas?: string[]): Promise + truncate(table: string, cascade?: boolean): Promise getAdvisoryLock(key: string | number, timeout?: number): Promise @@ -202,6 +206,11 @@ export interface QueryClientContract { */ getAllTypes(schemas?: string[]): Promise + /** + * Returns an array of all domain names + */ + getAllDomains(schemas?: string[]): Promise + /** * Drop all tables inside database */ @@ -217,6 +226,11 @@ export interface QueryClientContract { */ dropAllTypes(schemas?: string[]): Promise + /** + * Drop all domains inside the database + */ + dropAllDomains(schemas?: string[]): Promise + /** * Same as `query()`, but also selects the table for the query. The `from` method * doesn't allow defining the return type and one must use `query` to define diff --git a/test/database/views_types.spec.ts b/test/database/views_types.spec.ts index 02309019..f04c7f50 100644 --- a/test/database/views_types.spec.ts +++ b/test/database/views_types.spec.ts @@ -13,7 +13,7 @@ import { Connection } from '../../src/connection/index.js' import { QueryClient } from '../../src/query_client/index.js' import { getConfig, setup, cleanup, logger, createEmitter } from '../../test-helpers/index.js' -test.group('Query client | Views and types', (group) => { +test.group('Query client | Views, types and domains', (group) => { group.setup(async () => { await setup() }) @@ -101,5 +101,32 @@ test.group('Query client | Views and types', (group) => { assert.equal(types.length, 0) await connection.disconnect() }) + + test('Get all domains', async ({ assert }) => { + const connection = new Connection('primary', getConfig(), logger) + connection.connect() + const client = new QueryClient('dual', connection, createEmitter()) + + await client.rawQuery(`CREATE DOMAIN "email_domain" AS VARCHAR(255)`) + const domains = await client.getAllDomains(['public']) + + assert.isTrue(domains.includes('email_domain')) + + await client.dropAllDomains() + await connection.disconnect() + }) + + test('Drop all domains', async ({ assert }) => { + const connection = new Connection('primary', getConfig(), logger) + connection.connect() + const client = new QueryClient('dual', connection, createEmitter()) + + await client.rawQuery(`CREATE DOMAIN "email_domain" AS VARCHAR(255)`) + await client.dropAllDomains() + const domains = await client.getAllDomains() + + assert.isFalse(domains.includes('email_domain')) + await connection.disconnect() + }) } }) From 7b981e7424ceb330d49495e5f900c2bc8c06da7d Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Thu, 22 Feb 2024 19:32:34 +0100 Subject: [PATCH 19/73] chore(release): 20.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de87a953..e02d07d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.1.0", + "version": "20.2.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From 5c18c76f8bbb8c4ac1c31367390ef703f044f634 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Fri, 23 Feb 2024 13:01:50 +0100 Subject: [PATCH 20/73] refactor: change sqlite dropAllTables implementation (#1001) * refactor: change dropAllTables implementation * test: add test for dropTables with foreign keys constraint * refactor: handle pragma foreign_keys * fix: test for mssql * test: fix for mssql --- src/dialects/base_sqlite.ts | 36 ++++++++++++++------ test-helpers/index.ts | 6 ---- test/database/drop_tables.spec.ts | 55 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/dialects/base_sqlite.ts b/src/dialects/base_sqlite.ts index d81271e4..23aa1f94 100644 --- a/src/dialects/base_sqlite.ts +++ b/src/dialects/base_sqlite.ts @@ -91,16 +91,32 @@ export abstract class BaseSqliteDialect implements DialectContract { * Drop all tables inside the database */ async dropAllTables() { - await this.client.rawQuery('PRAGMA writable_schema = 1;') - await this.client - .knexQuery() - .delete() - .from('sqlite_master') - .whereIn('type', ['table', 'index', 'trigger']) - .whereNotIn('name', this.config.wipe?.ignoreTables || []) - - await this.client.rawQuery('PRAGMA writable_schema = 0;') - await this.client.rawQuery('VACUUM;') + const tables = await this.getAllTables() + + /** + * Check for foreign key pragma and turn it off if enabled + * so that we can drop tables without any issues + */ + const pragma = await this.client.rawQuery('PRAGMA foreign_keys;') + if (pragma[0].foreign_keys === 1) { + await this.client.rawQuery('PRAGMA foreign_keys = OFF;') + } + + /** + * Drop all tables + */ + const promises = tables + .filter((table) => !this.config.wipe?.ignoreTables?.includes(table)) + .map((table) => this.client.rawQuery(`DROP TABLE ${table};`)) + + await Promise.all(promises) + + /** + * Restore foreign key pragma to it's original value + */ + if (pragma[0].foreign_keys === 1) { + await this.client.rawQuery('PRAGMA foreign_keys = ON;') + } } /** diff --git a/test-helpers/index.ts b/test-helpers/index.ts index 0ad47835..53d4ce0a 100644 --- a/test-helpers/index.ts +++ b/test-helpers/index.ts @@ -74,12 +74,6 @@ export function getConfig(): ConnectionConfig { }, useNullAsDefault: true, debug: !!process.env.DEBUG, - pool: { - afterCreate(connection, done) { - connection.unsafeMode(true) - done() - }, - }, } case 'mysql': return { diff --git a/test/database/drop_tables.spec.ts b/test/database/drop_tables.spec.ts index 97ee6c3f..d863e354 100644 --- a/test/database/drop_tables.spec.ts +++ b/test/database/drop_tables.spec.ts @@ -107,4 +107,59 @@ test.group('Query client | drop tables', (group) => { await connection.disconnect() }) + + test('drop tables with foreign keys constraint', async ({ assert }) => { + const connection = new Connection('primary', getConfig(), logger) + connection.connect() + + await connection.client!.schema.createTableIfNotExists('temp_users', (table) => { + table.increments('id') + }) + + await connection.client!.schema.createTableIfNotExists('temp_posts', (table) => { + table.increments('id') + table.integer('temp_users_id').unsigned().references('id').inTable('temp_users') + }) + + await connection.client?.table('temp_users').insert({}) + const user = await connection.client?.table('temp_users').select('id').first() + await connection.client?.table('temp_posts').insert({ temp_users_id: user!.id }) + + const client = new QueryClient('dual', connection, createEmitter()) + await client.dialect.dropAllTables(['public']) + + assert.isFalse(await connection.client!.schema.hasTable('temp_users')) + assert.isFalse(await connection.client!.schema.hasTable('temp_posts')) + + await connection.disconnect() + }) + + if (['better-sqlite', 'sqlite'].includes(process.env.DB!)) { + test('drop tables when PRAGMA foreign_keys is enabled', async ({ assert }) => { + const connection = new Connection('primary', getConfig(), logger) + connection.connect() + + await connection.client!.schema.createTable('temp_posts', (table) => { + table.increments('id') + }) + + await connection.client!.schema.createTableIfNotExists('temp_users', (table) => { + table.increments('id') + table.integer('temp_posts_id').unsigned().references('id').inTable('temp_posts') + }) + + await connection.client?.table('temp_posts').insert({ id: 1 }) + await connection.client?.table('temp_users').insert({ id: 1, temp_posts_id: 1 }) + + const client = new QueryClient('dual', connection, createEmitter()) + + await client.rawQuery('PRAGMA foreign_keys = ON;') + await client.dialect.dropAllTables(['public']) + + assert.isFalse(await connection.client!.schema.hasTable('temp_users')) + assert.isFalse(await connection.client!.schema.hasTable('temp_posts')) + + await connection.disconnect() + }) + } }) From 018c4ce9cea956961a259ff5dba09a13e783d069 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 27 Feb 2024 14:50:38 +0530 Subject: [PATCH 21/73] chore(package): update dependencies --- package.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index e02d07d5..1edf2ea9 100644 --- a/package.json +++ b/package.json @@ -57,11 +57,11 @@ "index:commands": "adonis-kit index build/commands" }, "dependencies": { - "@adonisjs/presets": "^2.2.3", - "@faker-js/faker": "^8.3.1", + "@adonisjs/presets": "^2.2.5", + "@faker-js/faker": "^8.4.1", "@poppinss/hooks": "^7.2.2", "@poppinss/macroable": "^1.0.1", - "@poppinss/utils": "^6.7.1", + "@poppinss/utils": "^6.7.2", "fast-deep-equal": "^3.1.3", "igniculus": "^1.5.0", "kleur": "^4.1.5", @@ -73,42 +73,42 @@ "tarn": "^3.0.2" }, "devDependencies": { - "@adonisjs/assembler": "^7.1.0", - "@adonisjs/core": "^6.2.2", + "@adonisjs/assembler": "^7.2.2", + "@adonisjs/core": "^6.3.1", "@adonisjs/eslint-config": "^1.2.1", "@adonisjs/prettier-config": "^1.2.1", "@adonisjs/tsconfig": "^1.2.1", - "@commitlint/cli": "^18.5.0", - "@commitlint/config-conventional": "^18.5.0", + "@commitlint/cli": "^18.6.1", + "@commitlint/config-conventional": "^18.6.2", "@japa/assert": "^2.1.0", "@japa/file-system": "^2.2.0", "@japa/runner": "^3.1.1", - "@swc/core": "^1.3.105", + "@swc/core": "^1.4.2", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.11.6", + "@types/node": "^20.11.20", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.11", - "@vinejs/vine": "^1.7.0", - "better-sqlite3": "^9.3.0", + "@vinejs/vine": "^1.7.1", + "better-sqlite3": "^9.4.3", "c8": "^9.1.0", "chance": "^1.1.11", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "dotenv": "^16.4.1", - "eslint": "^8.56.0", + "dotenv": "^16.4.5", + "eslint": "^8.57.0", "fs-extra": "^11.2.0", "github-label-sync": "^2.3.1", - "husky": "^9.0.1", + "husky": "^9.0.11", "luxon": "^3.4.4", - "mysql2": "^3.8.0", - "np": "^9.2.0", + "mysql2": "^3.9.2", + "np": "^10.0.0", "pg": "^8.11.0", - "prettier": "^3.2.4", + "prettier": "^3.2.5", "reflect-metadata": "^0.2.0", "sqlite3": "^5.1.7", - "tedious": "^16.6.1", + "tedious": "^17.0.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, From 703e6b501f2f2b91693b255ef346ed829d602568 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 27 Feb 2024 15:38:44 +0530 Subject: [PATCH 22/73] feat: add model.lockForUpdate method to lock the model row for updates --- src/orm/base_model/index.ts | 24 +++++++ src/types/model.ts | 16 +++++ test/orm/base_model.spec.ts | 124 ++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index 1efbce2d..1ef649b2 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -1845,6 +1845,30 @@ class BaseModelImpl implements LucidRow { return this } + /** + * The lockForUpdate method re-fetches the model instance from + * the database and locks the row to perform an update. The + * provided callback receives a fresh user instance and should + * use that to perform an update. + */ + async lockForUpdate(callback: (user: this) => Promise | T): Promise { + const Model = this.constructor as LucidModel + const queryClient = Model.$adapter.modelClient(this) + + return managedTransaction(queryClient, async (trx) => { + const user = await Model.query({ client: trx }) + .forUpdate() + .where(Model.primaryKey, this.$primaryKeyValue) + .first() + + if (!user) { + throw new errors.E_MODEL_DELETED() + } + + return callback(user as this) + }) + } + /** * Perform delete by issuing a delete request on the adapter */ diff --git a/src/types/model.ts b/src/types/model.ts index a0bdc286..65efb4f6 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -619,7 +619,23 @@ export interface LucidRow { * Actions to perform on the instance */ save(): Promise + + /** + * The lockForUpdate method re-fetches the model instance from + * the database and locks the row to perform an update. The + * provided callback receives a fresh user instance and should + * use that to perform an update. + */ + lockForUpdate(callback: (user: this) => Promise | T): Promise + + /** + * Perform delete by issuing a delete request on the adapter + */ delete(): Promise + + /** + * Reload/Refresh the model instance + */ refresh(): Promise /** diff --git a/test/orm/base_model.spec.ts b/test/orm/base_model.spec.ts index 205c4b1b..27f2f4bb 100644 --- a/test/orm/base_model.spec.ts +++ b/test/orm/base_model.spec.ts @@ -7926,3 +7926,127 @@ test.group('Base model | inheritance', (group) => { assert.isFalse(MyBaseModel.$hooks.has('before:create', hook2)) }) }) + +test.group('Base Model | lockForUpdate', (group) => { + group.setup(async () => { + await setup() + }) + + group.teardown(async () => { + await cleanupTables() + }) + + test('lock model row for update', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + + @column() + declare points: number + } + + const user = await User.create({ email: 'foo@bar.com', username: 'virk', points: 0 }) + await Promise.all([ + user.lockForUpdate(async (freshUser) => { + freshUser.points = freshUser.points + 1 + await freshUser.save() + }), + user.lockForUpdate(async (freshUser) => { + freshUser.points = freshUser.points + 1 + await freshUser.save() + }), + ]) + + await user.refresh() + assert.equal(user.points, 2) + }) + + test('re-use initial model transaction', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + + @column() + declare points: number + } + + const user = await User.create({ email: 'foo@bar.com', username: 'virk', points: 0 }) + const trx = await db.transaction() + user.useTransaction(trx) + + await Promise.all([ + user.lockForUpdate(async (freshUser) => { + freshUser.points = freshUser.points + 1 + await freshUser.save() + }), + user.lockForUpdate(async (freshUser) => { + freshUser.points = freshUser.points + 1 + await freshUser.save() + }), + ]) + + assert.isFalse(trx.isCompleted) + await trx.rollback() + + await user.refresh() + assert.equal(user.points, 0) + }) + + test('throw error when row is missing', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + + @column() + declare points: number + } + + const user = await User.create({ email: 'foo@bar.com', username: 'virk', points: 0 }) + await user.delete() + await assert.rejects(() => + user.lockForUpdate(async (freshUser) => { + freshUser.points = freshUser.points + 1 + await freshUser.save() + }) + ) + }) +}) From 1b47ba76a40a262096be9153554c2608a3601792 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 27 Feb 2024 15:46:21 +0530 Subject: [PATCH 23/73] test: reset tables between tests --- test/orm/base_model.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/orm/base_model.spec.ts b/test/orm/base_model.spec.ts index 27f2f4bb..54ee7bab 100644 --- a/test/orm/base_model.spec.ts +++ b/test/orm/base_model.spec.ts @@ -7936,6 +7936,10 @@ test.group('Base Model | lockForUpdate', (group) => { await cleanupTables() }) + group.each.teardown(async () => { + await resetTables() + }) + test('lock model row for update', async ({ fs, assert }) => { const app = new AppFactory().create(fs.baseUrl, () => {}) await app.init() From 396c434d5ac03e768786d00d50a3e0cb59ee8b98 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 27 Feb 2024 15:52:10 +0530 Subject: [PATCH 24/73] chore(release): 20.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1edf2ea9..6d930f15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.2.0", + "version": "20.3.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From 2754e5c3300c1937220a3f3e9ed336aa69ee9fa1 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sun, 3 Mar 2024 16:26:23 +0100 Subject: [PATCH 25/73] fix(configure): correct call to logger.error --- configure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ts b/configure.ts index 807664ec..2e4bef33 100644 --- a/configure.ts +++ b/configure.ts @@ -38,7 +38,7 @@ export async function configure(command: Configure) { * Show error when selected dialect is not supported */ if (dialect! in DIALECTS === false) { - command.error( + command.logger.error( `The selected database "${dialect}" is invalid. Select one from: ${string.sentence( Object.keys(DIALECTS) )}` From 67ba4629aefd69cbfb097bee326ea1204066d90e Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 5 Mar 2024 16:31:27 +0100 Subject: [PATCH 26/73] chore: fix url in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b763d27..ad7bada1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ SQL ORM for AdonisJS built on top of Knex. Comes with a db query builder, Active record ORM, migrations, seeders and model factories. ## Official Documentation -The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/database/introduction) +The documentation is available on the [Lucid website](https://lucid.adonisjs.com/docs/introduction) ## Contributing One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. From 8c333e0f752fe7b0e73529e8ee81319bd4528298 Mon Sep 17 00:00:00 2001 From: Peter Cartwright Date: Sat, 9 Mar 2024 07:07:04 +0800 Subject: [PATCH 27/73] fix: omit double quotes from connection name (#983) When passing connection name parameter, connection name includes double quotes, so omit them fix #947 --- commands/migration/fresh.ts | 4 ++-- commands/migration/refresh.ts | 4 ++-- commands/migration/reset.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/commands/migration/fresh.ts b/commands/migration/fresh.ts index 9ee62356..757c78e3 100644 --- a/commands/migration/fresh.ts +++ b/commands/migration/fresh.ts @@ -73,7 +73,7 @@ export default class Refresh extends BaseCommand { } if (this.connection) { - args.push(`--connection="${this.connection}"`) + args.push(`--connection=${this.connection}`) } if (this.disableLocks) { @@ -127,7 +127,7 @@ export default class Refresh extends BaseCommand { private async runDbSeed() { const args: string[] = [] if (this.connection) { - args.push(`--connection="${this.connection}"`) + args.push(`--connection=${this.connection}`) } const dbSeed = await this.kernel.exec('db:seed', args) diff --git a/commands/migration/refresh.ts b/commands/migration/refresh.ts index dc7f7bf4..4bf45694 100644 --- a/commands/migration/refresh.ts +++ b/commands/migration/refresh.ts @@ -61,7 +61,7 @@ export default class Refresh extends BaseCommand { } if (this.connection) { - args.push(`--connection="${this.connection}"`) + args.push(`--connection=${this.connection}`) } if (this.dryRun) { @@ -99,7 +99,7 @@ export default class Refresh extends BaseCommand { private async runDbSeed() { const args: string[] = [] if (this.connection) { - args.push(`--connection="${this.connection}"`) + args.push(`--connection=${this.connection}`) } const dbSeed = await this.kernel.exec('db:seed', args) diff --git a/commands/migration/reset.ts b/commands/migration/reset.ts index 0aa98bd2..af4af090 100644 --- a/commands/migration/reset.ts +++ b/commands/migration/reset.ts @@ -55,7 +55,7 @@ export default class Reset extends BaseCommand { } if (this.connection) { - args.push(`--connection="${this.connection}"`) + args.push(`--connection=${this.connection}`) } if (this.dryRun) { From ea41f57e1239b89884829d822613de022e3195b1 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sat, 9 Mar 2024 00:01:27 +0100 Subject: [PATCH 28/73] feat(base_model): add findManyBy method --- src/orm/base_model/index.ts | 23 ++++++++++++++ src/types/model.ts | 17 +++++++++++ test/orm/base_model.spec.ts | 60 +++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index 1ef649b2..1a3430bd 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -721,6 +721,29 @@ class BaseModelImpl implements LucidRow { return this.query(options).where(key, value).firstOrFail() } + /** + * Find multiple models instance using a key/value pair + */ + // @ts-expect-error - Return type should be inferred when used in a model + static findManyBy(clause: Record, options?: ModelAdapterOptions) + // @ts-expect-error - Return type should be inferred when used in a model + static findManyBy(key: string, value: any[], options?: ModelAdapterOptions) + static findManyBy( + key: string | Record, + value?: any[] | ModelAdapterOptions, + options?: ModelAdapterOptions + ) { + if (typeof key === 'object') { + return this.query(value as ModelAdapterOptions).where(key) + } + + if (value === undefined) { + throw new Exception('"findManyBy" expects a value. Received undefined') + } + + return this.query(options).where(key, value) + } + /** * Same as `query().first()` */ diff --git a/src/types/model.ts b/src/types/model.ts index 65efb4f6..fbbf35a1 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -995,6 +995,23 @@ export interface LucidModel { options?: ModelAdapterOptions ): Promise> + /** + * Find multiple models instance using a key/value pair + */ + findManyBy( + clause: Record, + options?: ModelAdapterOptions + ): Promise[]> + + /** + * Find multiple models instance using a key/value pair + */ + findManyBy( + key: string, + value: any, + options?: ModelAdapterOptions + ): Promise[]> + /** * Same as `query().first()` */ diff --git a/test/orm/base_model.spec.ts b/test/orm/base_model.spec.ts index 54ee7bab..67796f1b 100644 --- a/test/orm/base_model.spec.ts +++ b/test/orm/base_model.spec.ts @@ -3780,6 +3780,66 @@ test.group('Base Model | fetch', (group) => { assert.equal(users[1].$primaryKeyValue, 1) }) + test('find many using a clause', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + } + + await db + .insertQuery() + .table('users') + .multiInsert([{ username: 'virk' }, { username: 'nikk' }]) + + const users = await User.findManyBy({ points: 0 }) + assert.lengthOf(users, 2) + assert.equal(users[0].$primaryKeyValue, 1) + assert.equal(users[1].$primaryKeyValue, 2) + }) + + test('find many using a key/value pair', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + } + + await db + .insertQuery() + .table('users') + .multiInsert([{ username: 'virk' }, { username: 'nikk' }]) + + const users = await User.findManyBy('points', 0) + assert.lengthOf(users, 2) + assert.equal(users[0].$primaryKeyValue, 1) + assert.equal(users[1].$primaryKeyValue, 2) + }) + test('return the existing row when search criteria matches', async ({ fs, assert }) => { const app = new AppFactory().create(fs.baseUrl, () => {}) await app.init() From deb0052983009b8ab760d59167eee7b0e81d508d Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 12 Mar 2024 11:31:39 +0100 Subject: [PATCH 29/73] fix(base_model): execute the query in findManyBy --- src/orm/base_model/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index 1a3430bd..f216c7cb 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -734,14 +734,16 @@ class BaseModelImpl implements LucidRow { options?: ModelAdapterOptions ) { if (typeof key === 'object') { - return this.query(value as ModelAdapterOptions).where(key) + return this.query(value as ModelAdapterOptions) + .where(key) + .exec() } if (value === undefined) { throw new Exception('"findManyBy" expects a value. Received undefined') } - return this.query(options).where(key, value) + return this.query(options).where(key, value).exec() } /** @@ -1982,8 +1984,8 @@ class BaseModelImpl implements LucidRow { result[relation.serializeAs] = Array.isArray(value) ? value.map((one) => one.serialize(relationOptions)) : value === null - ? null - : value.serialize(relationOptions) + ? null + : value.serialize(relationOptions) return result }, {}) From e0a2b0333ca5a0795fe6df55ab386530610588c0 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Tue, 12 Mar 2024 11:53:31 +0100 Subject: [PATCH 30/73] style: lint --- src/orm/base_model/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index f216c7cb..9cf34981 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -1984,8 +1984,8 @@ class BaseModelImpl implements LucidRow { result[relation.serializeAs] = Array.isArray(value) ? value.map((one) => one.serialize(relationOptions)) : value === null - ? null - : value.serialize(relationOptions) + ? null + : value.serialize(relationOptions) return result }, {}) From f3976b9e5f6ae327844b7a896ff1d414a84b53aa Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 13 Mar 2024 15:59:01 +0530 Subject: [PATCH 31/73] feat: add support for pretty print debug queries --- providers/database_provider.ts | 15 ++++++++++++++- src/database/main.ts | 2 +- src/types/database.ts | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/providers/database_provider.ts b/providers/database_provider.ts index 3c8164c2..d54ec607 100644 --- a/providers/database_provider.ts +++ b/providers/database_provider.ts @@ -16,6 +16,7 @@ import { QueryClient } from '../src/query_client/index.js' import { BaseModel } from '../src/orm/base_model/index.js' import { DatabaseTestUtils } from '../src/test_utils/database.js' import type { DatabaseConfig, DbQueryEventNode } from '../src/types/database.js' +import { emit } from 'node:process' /** * Extending AdonisJS types @@ -104,6 +105,16 @@ export default class DatabaseServiceProvider { }) } + /** + * Registeres a listener to pretty print debug queries + */ + protected async prettyPrintDebugQueries(db: Database) { + if (db.config.prettyPrintDebugQueries) { + const emitter = await this.app.container.make('emitter') + emitter.on('db:query', db.prettyPrint) + } + } + /** * Invoked by AdonisJS to register container bindings */ @@ -112,7 +123,8 @@ export default class DatabaseServiceProvider { const config = this.app.config.get('database') const emitter = await resolver.make('emitter') const logger = await resolver.make('logger') - return new Database(config, logger, emitter) + const db = new Database(config, logger, emitter) + return db }) this.app.container.singleton(QueryClient, async (resolver) => { @@ -131,6 +143,7 @@ export default class DatabaseServiceProvider { const db = await this.app.container.make('lucid.db') BaseModel.$adapter = new Adapter(db) + await this.prettyPrintDebugQueries(db) await this.registerTestUtils() await this.registerReplBindings() await this.registerVineJSRules(db) diff --git a/src/database/main.ts b/src/database/main.ts index a20318d5..bfc40548 100644 --- a/src/database/main.ts +++ b/src/database/main.ts @@ -57,7 +57,7 @@ export class Database extends Macroable { prettyPrint = prettyPrint constructor( - private config: DatabaseConfig, + public config: DatabaseConfig, private logger: Logger, private emitter: Emitter ) { diff --git a/src/types/database.ts b/src/types/database.ts index 4138d48d..47ae0f25 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -564,6 +564,7 @@ export type ConnectionConfig = */ export type DatabaseConfig = { connection: string + prettyPrintDebugQueries?: boolean connections: { [key: string]: ConnectionConfig } } From f989fb669c6fdaeeae4fe106d1dd920aefe3f0d2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 13 Mar 2024 16:09:27 +0530 Subject: [PATCH 32/73] style: remove unused imports --- providers/database_provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/providers/database_provider.ts b/providers/database_provider.ts index d54ec607..cb06a8f4 100644 --- a/providers/database_provider.ts +++ b/providers/database_provider.ts @@ -16,7 +16,6 @@ import { QueryClient } from '../src/query_client/index.js' import { BaseModel } from '../src/orm/base_model/index.js' import { DatabaseTestUtils } from '../src/test_utils/database.js' import type { DatabaseConfig, DbQueryEventNode } from '../src/types/database.js' -import { emit } from 'node:process' /** * Extending AdonisJS types From 8e1f3a76bca02fdd92f308318e4b825ea428c92d Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sun, 3 Mar 2024 16:27:33 +0100 Subject: [PATCH 33/73] chore(release): 20.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d930f15..d300cc72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.3.0", + "version": "20.3.1", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From f72005b96ee02b33fe59bd1427043ab9715d1b6d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 13 Mar 2024 16:12:20 +0530 Subject: [PATCH 34/73] chore(release): 20.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d300cc72..6f4627e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.3.1", + "version": "20.4.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From 40121291498af77d32f222c3400afe3a12bb9120 Mon Sep 17 00:00:00 2001 From: MaximeMRF Date: Thu, 14 Mar 2024 21:30:41 +0800 Subject: [PATCH 35/73] feat(Migrator): add step option --- README.md | 2 +- commands/migration/rollback.ts | 9 ++ src/migration/runner.ts | 12 ++- src/types/migrator.ts | 1 + test/migrations/migrator.spec.ts | 176 +++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ad7bada1..ca4e3491 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ We encourage you to read the [contribution guide](https://github.com/adonisjs/.g Easiest way to run tests is to launch the redis cluster using docker-compose and `docker-compose.yml` file. ```sh -docker-compose up +docker-compose up -d npm run test ``` diff --git a/commands/migration/rollback.ts b/commands/migration/rollback.ts index 592baee6..5a4d1fa9 100644 --- a/commands/migration/rollback.ts +++ b/commands/migration/rollback.ts @@ -52,6 +52,14 @@ export default class Rollback extends MigrationsBase { }) declare batch: number + /** + * Define custom step, instead of rolling back to the latest batch + */ + @flags.number({ + description: 'The number of migrations to be reverted', + }) + declare step: number + /** * Display migrations result in one compact single-line output */ @@ -74,6 +82,7 @@ export default class Rollback extends MigrationsBase { direction: 'down', connectionName: this.connection, batch: this.batch, + step: this.step, dryRun: this.dryRun, disableLocks: this.disableLocks, }) diff --git a/src/migration/runner.ts b/src/migration/runner.ts index 4531b645..8337ecb1 100644 --- a/src/migration/runner.ts +++ b/src/migration/runner.ts @@ -467,7 +467,7 @@ export class MigrationRunner extends EventEmitter { /** * Migrate down (aka rollback) */ - private async runDown(batch?: number) { + private async runDown(batch?: number, step?: number) { if (this.isInProduction && this.migrationsConfig.disableRollbacksInProduction) { throw new Error( 'Rollback in production environment is disabled. Check "config/database" file for options.' @@ -481,6 +481,12 @@ export class MigrationRunner extends EventEmitter { const existing = await this.getMigratedFilesTillBatch(batch) const collected = await this.migrationSource.getMigrations() + if (step === undefined || step <= 0) { + step = 0 + } else { + batch = (await this.getLatestBatch()) - 1 + } + /** * Finding schema files for migrations to rollback. We do not perform * rollback when any of the files are missing @@ -499,7 +505,7 @@ export class MigrationRunner extends EventEmitter { } }) - const filesToMigrate = Object.keys(this.migratedFiles) + const filesToMigrate = Object.keys(this.migratedFiles).slice(-step) for (let name of filesToMigrate) { await this.executeMigration(this.migratedFiles[name].file) } @@ -583,7 +589,7 @@ export class MigrationRunner extends EventEmitter { if (this.direction === 'up') { await this.runUp() } else if (this.options.direction === 'down') { - await this.runDown(this.options.batch) + await this.runDown(this.options.batch, this.options.step) } } catch (error) { this.error = error diff --git a/src/types/migrator.ts b/src/types/migrator.ts index a628cc2a..141f1541 100644 --- a/src/types/migrator.ts +++ b/src/types/migrator.ts @@ -22,6 +22,7 @@ export type MigratorOptions = | { direction: 'down' batch?: number + step?: number connectionName?: string dryRun?: boolean disableLocks?: boolean diff --git a/test/migrations/migrator.spec.ts b/test/migrations/migrator.spec.ts index 261f9641..4530f57e 100644 --- a/test/migrations/migrator.spec.ts +++ b/test/migrations/migrator.spec.ts @@ -640,6 +640,182 @@ test.group('Migrator', (group) => { ]) }) + test('rollback database using schema files to a given step', async ({ fs, assert, cleanup }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + cleanup(() => db.manager.closeAll()) + + await fs.create( + 'database/migrations/0_users_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_users', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_users') + } + } + ` + ) + + await fs.create( + 'database/migrations/1_accounts_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_accounts', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_accounts') + } + } + ` + ) + + const migrator = getMigrator(db, app, { direction: 'up', connectionName: 'primary' }) + await migrator.run() + + const migrator1 = getMigrator(db, app, { + direction: 'down', + step: 1, + connectionName: 'primary', + }) + await migrator1.run() + + const migrated = await db.connection().from('adonis_schema').select('*') + const hasUsersTable = await db.connection().schema.hasTable('schema_users') + const hasAccountsTable = await db.connection().schema.hasTable('schema_accounts') + const migratedFiles = Object.keys(migrator1.migratedFiles).map((file) => { + return { + status: migrator1.migratedFiles[file].status, + file: file, + queries: migrator1.migratedFiles[file].queries, + } + }) + + assert.lengthOf(migrated, 1) + assert.isFalse(hasUsersTable) + assert.isTrue(hasAccountsTable) + assert.deepEqual(migratedFiles, [ + { + status: 'pending', + file: 'database/migrations/1_accounts_v6', + queries: [], + }, + { + status: 'completed', + file: 'database/migrations/0_users_v6', + queries: [], + }, + ]) + }) + + test('negative numbers specified by the step option must rollback all the migrated files to the current batch', async ({ + fs, + assert, + cleanup, + }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + cleanup(() => db.manager.closeAll()) + + await fs.create( + 'database/migrations/0_users_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_users', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_users') + } + } + ` + ) + + const migrator = getMigrator(db, app, { direction: 'up', connectionName: 'primary' }) + await migrator.run() + + await fs.create( + 'database/migrations/1_accounts_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_accounts', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_accounts') + } + } + ` + ) + + await fs.create( + 'database/migrations/2_roles_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_roles', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_roles') + } + } + ` + ) + + const migrator1 = getMigrator(db, app, { direction: 'up', connectionName: 'primary' }) + await migrator.run() + + const migrator2 = getMigrator(db, app, { + direction: 'down', + step: -1, + connectionName: 'primary', + }) + await migrator2.run() + + const migrated = await db.connection().from('adonis_schema').select('*') + const hasUsersTable = await db.connection().schema.hasTable('schema_users') + const hasAccountsTable = await db.connection().schema.hasTable('schema_accounts') + const hasRolesTable = await db.connection().schema.hasTable('schema_roles') + const migratedFiles = Object.keys(migrator1.migratedFiles).map((file) => { + return { + status: migrator2.migratedFiles[file].status, + file: file, + queries: migrator2.migratedFiles[file].queries, + } + }) + + assert.lengthOf(migrated, 0) + assert.isFalse(hasUsersTable) + assert.isFalse(hasAccountsTable) + assert.isFalse(hasRolesTable) + assert.deepEqual(migratedFiles, []) + }) + test('rollback multiple times must be a noop', async ({ fs, assert, cleanup }) => { const app = new AppFactory().create(fs.baseUrl, () => {}) await app.init() From 5d87cf811d5ffaf42b19ea5f1c951b1d381fcdd8 Mon Sep 17 00:00:00 2001 From: adamcikado Date: Tue, 19 Mar 2024 11:55:22 -0400 Subject: [PATCH 36/73] feat: add missing exports --- package.json | 3 ++- src/orm/main.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f4627e1..e73c8c1e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "./services/*": "./build/services/*.js", "./types/*": "./build/src/types/*.js", "./migration": "./build/src/migration/main.js", - "./database_provider": "./build/providers/database_provider.js" + "./database_provider": "./build/providers/database_provider.js", + "./utils": "./build/src/utils/index.js" }, "scripts": { "pretest": "npm run lint", diff --git a/src/orm/main.ts b/src/orm/main.ts index 00931fa7..f9e55aa7 100644 --- a/src/orm/main.ts +++ b/src/orm/main.ts @@ -14,3 +14,4 @@ export { BaseModel, scope } from './base_model/index.js' export { ModelQueryBuilder } from './query_builder/index.js' export { SnakeCaseNamingStrategy } from './naming_strategies/snake_case.js' export { CamelCaseNamingStrategy } from './naming_strategies/camel_case.js' +export { Preloader } from './preloader/index.js' From c22f0197367255d3c1658ac5f9b1a3ffdaa3f840 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 26 Mar 2024 21:59:44 +0100 Subject: [PATCH 37/73] fix(base_model): correct typing for findManyBy Closes https://github.com/adonisjs/lucid/issues/1014 --- src/types/model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/model.ts b/src/types/model.ts index fbbf35a1..cd0d5d03 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -999,6 +999,7 @@ export interface LucidModel { * Find multiple models instance using a key/value pair */ findManyBy( + this: T, clause: Record, options?: ModelAdapterOptions ): Promise[]> @@ -1007,6 +1008,7 @@ export interface LucidModel { * Find multiple models instance using a key/value pair */ findManyBy( + this: T, key: string, value: any, options?: ModelAdapterOptions From 059c4722c64e7dc2cd09c6ab5d7745ef0d99eb75 Mon Sep 17 00:00:00 2001 From: Romain Lanz <2793951+RomainLanz@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:07:09 +0100 Subject: [PATCH 38/73] feat(base_model): add clause variant to findBy method (#1020) * feat(base_model): add findBy and findByOrFail clause alternative * test(base_model): add test about findBy --- src/orm/base_model/index.ts | 33 +++++++++++++++++++-- src/types/model.ts | 20 ++++++++++++- test/orm/base_model.spec.ts | 58 +++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index 9cf34981..57204c10 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -703,7 +703,21 @@ class BaseModelImpl implements LucidRow { /** * Find model instance using a key/value pair */ - static async findBy(key: string, value: any, options?: ModelAdapterOptions) { + // @ts-expect-error - Return type should be inferred when used in a model + static findBy(clause: Record, options?: ModelAdapterOptions) + // @ts-expect-error - Return type should be inferred when used in a model + static findBy(key: string, value: any, options?: ModelAdapterOptions) + static async findBy( + key: string | Record, + value?: any | ModelAdapterOptions, + options?: ModelAdapterOptions + ) { + if (typeof key === 'object') { + return this.query(value as ModelAdapterOptions) + .where(key) + .first() + } + if (value === undefined) { throw new Exception('"findBy" expects a value. Received undefined') } @@ -714,10 +728,25 @@ class BaseModelImpl implements LucidRow { /** * Find model instance using a key/value pair */ - static async findByOrFail(key: string, value: any, options?: ModelAdapterOptions) { + // @ts-expect-error - Return type should be inferred when used in a model + static findByOrFail(clause: Record, options?: ModelAdapterOptions) + // @ts-expect-error - Return type should be inferred when used in a model + static findByOrFail(key: string, value: any, options?: ModelAdapterOptions) + static async findByOrFail( + key: string | Record, + value?: any | ModelAdapterOptions, + options?: ModelAdapterOptions + ) { + if (typeof key === 'object') { + return this.query(value as ModelAdapterOptions) + .where(key) + .firstOrFail() + } + if (value === undefined) { throw new Exception('"findByOrFail" expects a value. Received undefined') } + return this.query(options).where(key, value).firstOrFail() } diff --git a/src/types/model.ts b/src/types/model.ts index cd0d5d03..7a2d7e5c 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -975,6 +975,15 @@ export interface LucidModel { options?: ModelAdapterOptions ): Promise> + /** + * Find one using a clause + */ + findBy( + this: T, + clause: Record, + options?: ModelAdapterOptions + ): Promise> + /** * Find one using a key-value pair */ @@ -985,6 +994,15 @@ export interface LucidModel { options?: ModelAdapterOptions ): Promise> + /** + * Find one using a clause or fail + */ + findByOrFail( + this: T, + clause: Record, + options?: ModelAdapterOptions + ): Promise> + /** * Find one using a key-value pair or fail */ @@ -996,7 +1014,7 @@ export interface LucidModel { ): Promise> /** - * Find multiple models instance using a key/value pair + * Find multiple models instance using a clause */ findManyBy( this: T, diff --git a/test/orm/base_model.spec.ts b/test/orm/base_model.spec.ts index 67796f1b..6556877b 100644 --- a/test/orm/base_model.spec.ts +++ b/test/orm/base_model.spec.ts @@ -3780,6 +3780,64 @@ test.group('Base Model | fetch', (group) => { assert.equal(users[1].$primaryKeyValue, 1) }) + test('findBy using a clause', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + } + + await db + .insertQuery() + .table('users') + .multiInsert([{ username: 'virk' }, { username: 'nikk' }]) + + const user = await User.findBy({ username: 'virk' }) + assert.isDefined(user) + assert.equal(user?.username, 'virk') + }) + + test('findBy using a key/value pair', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + } + + await db + .insertQuery() + .table('users') + .multiInsert([{ username: 'virk' }, { username: 'nikk' }]) + + const user = await User.findBy('username', 'virk') + assert.isDefined(user) + assert.equal(user?.username, 'virk') + }) + test('find many using a clause', async ({ fs, assert }) => { const app = new AppFactory().create(fs.baseUrl, () => {}) await app.init() From e96f5faf7d272f81bd7684a7632efbc6809f44e8 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 26 Mar 2024 22:12:43 +0100 Subject: [PATCH 39/73] chore(release): 20.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f4627e1..3b382a75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.4.0", + "version": "20.5.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From 5002dc9970df5ae8b227a8233af908c2e03ab058 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 26 Mar 2024 22:13:27 +0100 Subject: [PATCH 40/73] chore(release): 20.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b382a75..f4219359 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.5.0", + "version": "20.5.1", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From 781c479898991555fbf312e61d7bae4ac0a9a689 Mon Sep 17 00:00:00 2001 From: MaximeMRF Date: Wed, 3 Apr 2024 17:02:15 +0800 Subject: [PATCH 41/73] fix(migrator): step option --- src/migration/runner.ts | 5 +++-- test/migrations/migrator.spec.ts | 11 +++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/migration/runner.ts b/src/migration/runner.ts index 8337ecb1..b4f7ac7d 100644 --- a/src/migration/runner.ts +++ b/src/migration/runner.ts @@ -482,7 +482,7 @@ export class MigrationRunner extends EventEmitter { const collected = await this.migrationSource.getMigrations() if (step === undefined || step <= 0) { - step = 0 + step = collected.length } else { batch = (await this.getLatestBatch()) - 1 } @@ -505,7 +505,8 @@ export class MigrationRunner extends EventEmitter { } }) - const filesToMigrate = Object.keys(this.migratedFiles).slice(-step) + this.migratedFiles = Object.fromEntries(Object.entries(this.migratedFiles).slice(0, step)) + const filesToMigrate = Object.keys(this.migratedFiles) for (let name of filesToMigrate) { await this.executeMigration(this.migratedFiles[name].file) } diff --git a/test/migrations/migrator.spec.ts b/test/migrations/migrator.spec.ts index 4530f57e..b981b2f9 100644 --- a/test/migrations/migrator.spec.ts +++ b/test/migrations/migrator.spec.ts @@ -704,17 +704,12 @@ test.group('Migrator', (group) => { }) assert.lengthOf(migrated, 1) - assert.isFalse(hasUsersTable) - assert.isTrue(hasAccountsTable) + assert.isTrue(hasUsersTable) + assert.isFalse(hasAccountsTable) assert.deepEqual(migratedFiles, [ - { - status: 'pending', - file: 'database/migrations/1_accounts_v6', - queries: [], - }, { status: 'completed', - file: 'database/migrations/0_users_v6', + file: 'database/migrations/1_accounts_v6', queries: [], }, ]) From 52906a5cf32c61e774bf8fba2936f92b12b340d1 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sun, 21 Apr 2024 14:07:33 +0200 Subject: [PATCH 42/73] chore: update version & add cross-env --- package.json | 68 ++++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index f4219359..30c3d54c 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,14 @@ }, "scripts": { "pretest": "npm run lint", - "test:better_sqlite": "DB=better_sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", - "test:sqlite": "DB=sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", - "test:mysql": "DB=mysql node --enable-source-maps --loader=ts-node/esm ./bin/test.js", - "test:mysql_legacy": "DB=mysql_legacy node --enable-source-maps --loader=ts-node/esm ./bin/test.js", - "test:mssql": "DB=mssql node --enable-source-maps --loader=ts-node/esm ./bin/test.js", - "test:pg": "DB=pg node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "test:better_sqlite": "cross-env DB=better_sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "test:sqlite": "cross-env DB=sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "test:mysql": "cross-env DB=mysql node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "test:mysql_legacy": "cross-env DB=mysql_legacy node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "test:mssql": "cross-env DB=mssql node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "test:pg": "cross-env DB=pg node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "test:docker": "npm run test:mysql && npm run test:mysql_legacy && npm run test:pg && npm run test:mssql", - "quick:test": "DB=sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "quick:test": "cross-env DB=sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "lint": "eslint . --ext=.ts", "clean": "del-cli build", "compile": "npm run lint && npm run clean && tsc", @@ -57,60 +57,60 @@ "index:commands": "adonis-kit index build/commands" }, "dependencies": { - "@adonisjs/presets": "^2.2.5", + "@adonisjs/presets": "^2.4.0", "@faker-js/faker": "^8.4.1", - "@poppinss/hooks": "^7.2.2", - "@poppinss/macroable": "^1.0.1", - "@poppinss/utils": "^6.7.2", + "@poppinss/hooks": "^7.2.3", + "@poppinss/macroable": "^1.0.2", + "@poppinss/utils": "^6.7.3", "fast-deep-equal": "^3.1.3", "igniculus": "^1.5.0", "kleur": "^4.1.5", "knex": "^3.1.0", "knex-dynamic-connection": "^3.1.1", "pretty-hrtime": "^1.0.3", - "qs": "^6.11.2", + "qs": "^6.12.1", "slash": "^5.1.0", "tarn": "^3.0.2" }, "devDependencies": { - "@adonisjs/assembler": "^7.2.2", - "@adonisjs/core": "^6.3.1", - "@adonisjs/eslint-config": "^1.2.1", - "@adonisjs/prettier-config": "^1.2.1", - "@adonisjs/tsconfig": "^1.2.1", - "@commitlint/cli": "^18.6.1", - "@commitlint/config-conventional": "^18.6.2", - "@japa/assert": "^2.1.0", - "@japa/file-system": "^2.2.0", - "@japa/runner": "^3.1.1", - "@swc/core": "^1.4.2", + "@adonisjs/assembler": "^7.5.0", + "@adonisjs/core": "^6.7.1", + "@adonisjs/eslint-config": "^1.3.0", + "@adonisjs/prettier-config": "^1.3.0", + "@adonisjs/tsconfig": "^1.3.0", + "@commitlint/cli": "^19.2.2", + "@commitlint/config-conventional": "^19.2.2", + "@japa/assert": "^3.0.0", + "@japa/file-system": "^2.3.0", + "@japa/runner": "^3.1.4", + "@swc/core": "^1.4.16", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.11.20", + "@types/node": "^20.12.7", "@types/pretty-hrtime": "^1.0.3", - "@types/qs": "^6.9.11", - "@vinejs/vine": "^1.7.1", - "better-sqlite3": "^9.4.3", + "@types/qs": "^6.9.15", + "@vinejs/vine": "^2.0.0", + "better-sqlite3": "^9.5.0", "c8": "^9.1.0", "chance": "^1.1.11", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", - "del-cli": "^5.0.0", + "del-cli": "^5.1.0", "dotenv": "^16.4.5", "eslint": "^8.57.0", "fs-extra": "^11.2.0", "github-label-sync": "^2.3.1", "husky": "^9.0.11", "luxon": "^3.4.4", - "mysql2": "^3.9.2", - "np": "^10.0.0", - "pg": "^8.11.0", + "mysql2": "^3.9.7", + "np": "^10.0.5", + "pg": "^8.11.5", "prettier": "^3.2.5", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "sqlite3": "^5.1.7", - "tedious": "^17.0.0", + "tedious": "^18.1.0", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.4.5" }, "peerDependencies": { "@adonisjs/assembler": "^7.0.0", From 7315d0e8ed3a3bad68f433a77d19622cabd38672 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Sun, 21 Apr 2024 14:12:13 +0200 Subject: [PATCH 43/73] chore: update postgresql version --- docker-compose.yml => compose.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) rename docker-compose.yml => compose.yml (97%) diff --git a/docker-compose.yml b/compose.yml similarity index 97% rename from docker-compose.yml rename to compose.yml index ecfee9ca..91ebc25d 100644 --- a/docker-compose.yml +++ b/compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: legacy_mysql: platform: linux/x86_64 @@ -48,7 +46,7 @@ services: - $LEGACY_MYSQL_READ_REPLICA_PORT pg: - image: postgres:11 + image: postgres:16 container_name: pg env_file: ./.env environment: @@ -61,7 +59,7 @@ services: - $PG_PORT pg_read_replica: - image: postgres:11 + image: postgres:16 container_name: pg_read_replica env_file: ./.env environment: From ba5961bc9c4c10a21ec202ce713f7663e7b48057 Mon Sep 17 00:00:00 2001 From: Michalis Giannas Date: Thu, 25 Apr 2024 16:44:43 +0200 Subject: [PATCH 44/73] fix: some typos (#1024) --- commands/db_truncate.ts | 4 ++-- commands/db_wipe.ts | 4 ++-- commands/migration/_base.ts | 2 +- commands/migration/rollback.ts | 4 ++-- commands/migration/run.ts | 2 +- src/bindings/vinejs.ts | 4 ++-- src/connection/manager.ts | 2 +- src/database/query_builder/chainable.ts | 2 +- src/orm/relations/belongs_to/query_builder.ts | 4 ++-- src/orm/relations/has_many/query_builder.ts | 2 +- src/orm/relations/has_many_through/query_builder.ts | 2 +- src/orm/relations/has_one/query_builder.ts | 2 +- src/orm/relations/many_to_many/pivot_helpers.ts | 12 ++++++------ src/orm/relations/many_to_many/query_builder.ts | 2 +- src/seeders/runner.ts | 2 +- src/types/database.ts | 6 +++--- 16 files changed, 28 insertions(+), 28 deletions(-) diff --git a/commands/db_truncate.ts b/commands/db_truncate.ts index 520aa331..d0dc5bf1 100644 --- a/commands/db_truncate.ts +++ b/commands/db_truncate.ts @@ -43,7 +43,7 @@ export default class DbTruncate extends BaseCommand { /** * Prompts to take consent when truncating the database in production */ - private async takeProductionConstent(): Promise { + private async takeProductionConsent(): Promise { const question = 'You are in production environment. Want to continue truncating the database?' try { return await this.prompt.confirm(question) @@ -78,7 +78,7 @@ export default class DbTruncate extends BaseCommand { */ let continueTruncate = !this.app.inProduction || this.force if (!continueTruncate) { - continueTruncate = await this.takeProductionConstent() + continueTruncate = await this.takeProductionConsent() } /** diff --git a/commands/db_wipe.ts b/commands/db_wipe.ts index aab3cc4a..3d86b4bd 100644 --- a/commands/db_wipe.ts +++ b/commands/db_wipe.ts @@ -61,7 +61,7 @@ export default class DbWipe extends BaseCommand { /** * Prompts to take consent when wiping the database in production */ - private async takeProductionConstent(): Promise { + private async takeProductionConsent(): Promise { const question = 'You are in production environment. Want to continue wiping the database?' try { return await this.prompt.confirm(question) @@ -141,7 +141,7 @@ export default class DbWipe extends BaseCommand { */ let continueWipe = !this.app.inProduction || this.force if (!continueWipe) { - continueWipe = await this.takeProductionConstent() + continueWipe = await this.takeProductionConsent() } /** diff --git a/commands/migration/_base.ts b/commands/migration/_base.ts index c139e521..202383d2 100644 --- a/commands/migration/_base.ts +++ b/commands/migration/_base.ts @@ -36,7 +36,7 @@ export default abstract class MigrationsBase extends BaseCommand { /** * Prompts to take consent for running migrations in production */ - protected async takeProductionConstent(): Promise { + protected async takeProductionConsent(): Promise { const question = 'You are in production environment. Want to continue running migrations?' try { return await this.prompt.confirm(question) diff --git a/commands/migration/rollback.ts b/commands/migration/rollback.ts index 5a4d1fa9..06ac2406 100644 --- a/commands/migration/rollback.ts +++ b/commands/migration/rollback.ts @@ -35,7 +35,7 @@ export default class Rollback extends MigrationsBase { /** * Force run migrations in production */ - @flags.boolean({ description: 'Explictly force to run migrations in production' }) + @flags.boolean({ description: 'Explicitly force to run migrations in production' }) declare force: boolean /** @@ -102,7 +102,7 @@ export default class Rollback extends MigrationsBase { */ let continueMigrations = !this.app.inProduction || this.force if (!continueMigrations) { - continueMigrations = await this.takeProductionConstent() + continueMigrations = await this.takeProductionConsent() } /** diff --git a/commands/migration/run.ts b/commands/migration/run.ts index e7b0a0cc..b2beb56e 100644 --- a/commands/migration/run.ts +++ b/commands/migration/run.ts @@ -83,7 +83,7 @@ export default class Migrate extends MigrationsBase { */ let continueMigrations = !this.app.inProduction || this.force if (!continueMigrations) { - continueMigrations = await this.takeProductionConstent() + continueMigrations = await this.takeProductionConsent() } /** diff --git a/src/bindings/vinejs.ts b/src/bindings/vinejs.ts index 1db97322..a048350e 100644 --- a/src/bindings/vinejs.ts +++ b/src/bindings/vinejs.ts @@ -21,8 +21,8 @@ export function defineValidationRules(db: Database) { return } - const isUnqiue = await checker(db, value as string, field) - if (!isUnqiue) { + const isUnique = await checker(db, value as string, field) + if (!isUnique) { field.report('The {{ field }} has already been taken', 'database.unique', field) } } diff --git a/src/connection/manager.ts b/src/connection/manager.ts index 1d501b2b..e6cfd020 100644 --- a/src/connection/manager.ts +++ b/src/connection/manager.ts @@ -159,7 +159,7 @@ export class ConnectionManager implements ConnectionManagerContract { /** * Move the current connection to the orphan connections. We need - * to keep a seperate track of old connections to make sure + * to keep a separate track of old connections to make sure * they cleanup after some time */ if (connection.connection) { diff --git a/src/database/query_builder/chainable.ts b/src/database/query_builder/chainable.ts index 0ff78051..7839ff3e 100644 --- a/src/database/query_builder/chainable.ts +++ b/src/database/query_builder/chainable.ts @@ -18,7 +18,7 @@ import { RawBuilder } from '../static_builder/raw.js' import { ReferenceBuilder } from '../static_builder/reference.js' /** - * The chainable query builder to consturct SQL queries for selecting, updating and + * The chainable query builder to construct SQL queries for selecting, updating and * deleting records. * * The API internally uses the knex query builder. However, many of methods may have diff --git a/src/orm/relations/belongs_to/query_builder.ts b/src/orm/relations/belongs_to/query_builder.ts index b3b1995e..6da081f4 100644 --- a/src/orm/relations/belongs_to/query_builder.ts +++ b/src/orm/relations/belongs_to/query_builder.ts @@ -62,7 +62,7 @@ export class BelongsToQueryBuilder extends BaseQueryBuilder { } /** - * The profiler data for belongsTo relatioship + * The profiler data for belongsTo relationship */ protected profilerData() { return { @@ -92,7 +92,7 @@ export class BelongsToQueryBuilder extends BaseQueryBuilder { const queryAction = this.queryAction() /** - * Eager query contraints + * Eager query constraints */ if (Array.isArray(this.parent)) { const foreignKeyValues = this.parent diff --git a/src/orm/relations/has_many/query_builder.ts b/src/orm/relations/has_many/query_builder.ts index 05afa8e1..9fd253af 100644 --- a/src/orm/relations/has_many/query_builder.ts +++ b/src/orm/relations/has_many/query_builder.ts @@ -95,7 +95,7 @@ export class HasManyQueryBuilder this.appliedConstraints = true /** - * Eager query contraints + * Eager query constraints */ if (Array.isArray(this.parent)) { this.wrapExisting().whereIn( diff --git a/src/orm/relations/has_many_through/query_builder.ts b/src/orm/relations/has_many_through/query_builder.ts index 9b16a422..aa32dc3e 100644 --- a/src/orm/relations/has_many_through/query_builder.ts +++ b/src/orm/relations/has_many_through/query_builder.ts @@ -75,7 +75,7 @@ export class HasManyThroughQueryBuilder const queryAction = this.queryAction() /** - * Eager query contraints + * Eager query constraints */ if (Array.isArray(this.parent)) { builder.whereIn( diff --git a/src/orm/relations/has_one/query_builder.ts b/src/orm/relations/has_one/query_builder.ts index 5a10bb57..7f17e7ad 100644 --- a/src/orm/relations/has_one/query_builder.ts +++ b/src/orm/relations/has_one/query_builder.ts @@ -90,7 +90,7 @@ export class HasOneQueryBuilder extends BaseQueryBuilder { const queryAction = this.queryAction() /** - * Eager query contraints + * Eager query constraints */ if (Array.isArray(this.parent)) { this.wrapExisting().whereIn( diff --git a/src/orm/relations/many_to_many/pivot_helpers.ts b/src/orm/relations/many_to_many/pivot_helpers.ts index 7885a230..c23d3ba6 100644 --- a/src/orm/relations/many_to_many/pivot_helpers.ts +++ b/src/orm/relations/many_to_many/pivot_helpers.ts @@ -45,10 +45,10 @@ export class PivotHelpers { /** * Adds a where pivot condition to the query */ - wherePivot(varition: 'or' | 'and' | 'not' | 'orNot', key: any, operator?: any, value?: any) { + wherePivot(variation: 'or' | 'and' | 'not' | 'orNot', key: any, operator?: any, value?: any) { let method: keyof (ManyToManyQueryBuilder | ManyToManySubQueryBuilder) = 'where' - switch (varition) { + switch (variation) { case 'or': method = 'orWhere' break @@ -71,10 +71,10 @@ export class PivotHelpers { /** * Adds a where pivot condition to the query */ - whereNullPivot(varition: 'or' | 'and' | 'not' | 'orNot', key: string) { + whereNullPivot(variation: 'or' | 'and' | 'not' | 'orNot', key: string) { let method: keyof (ManyToManyQueryBuilder | ManyToManySubQueryBuilder) = 'whereNull' - switch (varition) { + switch (variation) { case 'or': method = 'orWhereNull' break @@ -91,10 +91,10 @@ export class PivotHelpers { /** * Adds a where pivot condition to the query */ - whereInPivot(varition: 'or' | 'and' | 'not' | 'orNot', key: any, value: any) { + whereInPivot(variation: 'or' | 'and' | 'not' | 'orNot', key: any, value: any) { let method: keyof (ManyToManyQueryBuilder | ManyToManySubQueryBuilder) = 'whereIn' - switch (varition) { + switch (variation) { case 'or': method = 'orWhereIn' break diff --git a/src/orm/relations/many_to_many/query_builder.ts b/src/orm/relations/many_to_many/query_builder.ts index dc05d1dc..28fc107a 100644 --- a/src/orm/relations/many_to_many/query_builder.ts +++ b/src/orm/relations/many_to_many/query_builder.ts @@ -109,7 +109,7 @@ export class ManyToManyQueryBuilder const queryAction = this.queryAction() /** - * Eager query contraints + * Eager query constraints */ if (Array.isArray(this.parent)) { this.wrapExisting().whereInPivot( diff --git a/src/seeders/runner.ts b/src/seeders/runner.ts index d7c100a1..7be8b207 100644 --- a/src/seeders/runner.ts +++ b/src/seeders/runner.ts @@ -65,7 +65,7 @@ export class SeedsRunner { } /** - * Ignore when when the node environement is not the same as the seeder configuration. + * Ignore when the node environment is not the same as the seeder configuration. */ if (Source.environment && !Source.environment.includes(this.nodeEnvironment)) { seeder.status = 'ignored' diff --git a/src/types/database.ts b/src/types/database.ts index 47ae0f25..7faf184d 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -95,7 +95,7 @@ export interface TransactionFn { } /** - * Shape of the query client, that is used to retrive instances + * Shape of the query client, that is used to retrieve instances * of query builder */ export interface QueryClientContract { @@ -117,13 +117,13 @@ export interface QueryClientContract { readonly mode: 'dual' | 'write' | 'read' /** - * The name of the connnection from which the client + * The name of the connection from which the client * was originated */ readonly connectionName: string /** - * Is debug enabled on the connnection or not. Also opens up the API to + * Is debug enabled on the connection or not. Also opens up the API to * disable debug for a given client */ debug: boolean From 669bdb31e820a4d66a1f6701fcc143ce29dff3a1 Mon Sep 17 00:00:00 2001 From: adamcikado Date: Sat, 27 Jan 2024 10:29:40 +0100 Subject: [PATCH 45/73] fix: compare DateTime in newUpIfMissing --- src/orm/base_model/index.ts | 36 +++++++++++---------- src/utils/index.ts | 33 +++++++++++++++++++- test/orm/base_model.spec.ts | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 17 deletions(-) diff --git a/src/orm/base_model/index.ts b/src/orm/base_model/index.ts index 1efbce2d..12299a74 100644 --- a/src/orm/base_model/index.ts +++ b/src/orm/base_model/index.ts @@ -8,7 +8,6 @@ */ import { DateTime } from 'luxon' -import equal from 'fast-deep-equal' import Hooks from '@poppinss/hooks' import lodash from '@poppinss/utils/lodash' import { Exception, defineStaticProperty } from '@poppinss/utils' @@ -63,6 +62,8 @@ import { ensureRelation, managedTransaction, normalizeCherryPickObject, + transformDateValue, + compareValues, } from '../../utils/index.js' const MANY_RELATIONS = ['hasMany', 'manyToMany', 'hasManyThrough'] @@ -207,9 +208,13 @@ class BaseModelImpl implements LucidRow { * array */ return rowObjects.map((rowObject: any) => { - const existingRow = existingRows.find((one: any) => { - /* eslint-disable-next-line eqeqeq */ - return keys.every((key) => one[key] == rowObject[key]) + const existingRow = existingRows.find((row: any) => { + return keys.every((key) => { + const objectValue = rowObject[key] + const rowValue = row[key] + + return compareValues(rowValue, objectValue) + }) }) /** @@ -852,6 +857,8 @@ class BaseModelImpl implements LucidRow { payload: any, options?: ModelAssignOptions ): Promise { + const client = this.$adapter.modelConstructorClient(this as LucidModel, options) + uniqueKeys = Array.isArray(uniqueKeys) ? uniqueKeys : [uniqueKeys] const uniquenessPair: { key: string; value: string[] }[] = uniqueKeys.map( (uniqueKey: string) => { @@ -861,7 +868,7 @@ class BaseModelImpl implements LucidRow { throw new Exception( `Value for the "${uniqueKey}" is null or undefined inside "fetchOrNewUpMany" payload` ) - }), + }).map((value) => transformDateValue(value, client.dialect)), } } ) @@ -896,6 +903,8 @@ class BaseModelImpl implements LucidRow { payload: any, options?: ModelAssignOptions ): Promise { + const client = this.$adapter.modelConstructorClient(this as LucidModel, options) + uniqueKeys = Array.isArray(uniqueKeys) ? uniqueKeys : [uniqueKeys] const uniquenessPair: { key: string; value: string[] }[] = uniqueKeys.map( (uniqueKey: string) => { @@ -905,7 +914,7 @@ class BaseModelImpl implements LucidRow { throw new Exception( `Value for the "${uniqueKey}" is null or undefined inside "fetchOrCreateMany" payload` ) - }), + }).map((value) => transformDateValue(value, client.dialect)), } } ) @@ -960,6 +969,8 @@ class BaseModelImpl implements LucidRow { payload: any, options?: ModelAssignOptions ): Promise { + const client = this.$adapter.modelConstructorClient(this as LucidModel, options) + uniqueKeys = Array.isArray(uniqueKeys) ? uniqueKeys : [uniqueKeys] const uniquenessPair: { key: string; value: string[] }[] = uniqueKeys.map( (uniqueKey: string) => { @@ -969,13 +980,11 @@ class BaseModelImpl implements LucidRow { throw new Exception( `Value for the "${uniqueKey}" is null or undefined inside "updateOrCreateMany" payload` ) - }), + }).map((value) => transformDateValue(value, client.dialect)), } } ) - const client = this.$adapter.modelConstructorClient(this as LucidModel, options) - return managedTransaction(client, async (trx) => { /** * Find existing rows @@ -1287,15 +1296,10 @@ class BaseModelImpl implements LucidRow { const originalValue = this.$original[key] let isEqual = true - if (DateTime.isDateTime(value) || DateTime.isDateTime(originalValue)) { - isEqual = - DateTime.isDateTime(value) && DateTime.isDateTime(originalValue) - ? value.equals(originalValue) - : value === originalValue - } else if (isObject(value) && 'isDirty' in value) { + if (isObject(value) && 'isDirty' in value) { isEqual = !value.isDirty } else { - isEqual = equal(originalValue, value) + isEqual = compareValues(originalValue, value) } if (!isEqual) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 2af3ab10..d239aa79 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,9 +12,16 @@ import { join, extname } from 'node:path' import { Exception, fsReadAll, isScriptFile } from '@poppinss/utils' import { RelationshipsContract } from '../types/relations.js' import { LucidRow, ModelObject, CherryPickFields } from '../types/model.js' -import { FileNode, QueryClientContract, TransactionClientContract } from '../types/database.js' +import { + DialectContract, + FileNode, + QueryClientContract, + TransactionClientContract, +} from '../types/database.js' import { fileURLToPath, pathToFileURL } from 'node:url' import * as errors from '../errors.js' +import { DateTime } from 'luxon' +import equal from 'fast-deep-equal' /** * Ensure that relation is defined @@ -53,6 +60,30 @@ export function collectValues(payload: any[], key: string, missingCallback: () = }) } +/** + * Transform value if it is an instance of DateTime, so it can be processed by query builder + */ +export function transformDateValue(value: unknown, dialect: DialectContract) { + if (DateTime.isDateTime(value)) { + return value.toFormat(dialect.dateTimeFormat) + } + + return value +} + +/** + * Compare two values deeply whether they are equal or not + */ +export function compareValues(valueA: unknown, valueB: unknown) { + if (DateTime.isDateTime(valueA) || DateTime.isDateTime(valueB)) { + return DateTime.isDateTime(valueA) && DateTime.isDateTime(valueB) + ? valueA.equals(valueB) + : valueA === valueB + } else { + return equal(valueA, valueB) + } +} + /** * Raises exception when a relationship `booted` property is false. */ diff --git a/test/orm/base_model.spec.ts b/test/orm/base_model.spec.ts index 205c4b1b..3ceaf54c 100644 --- a/test/orm/base_model.spec.ts +++ b/test/orm/base_model.spec.ts @@ -4888,6 +4888,68 @@ test.group('Base Model | fetch', (group) => { assert.lengthOf(usersList, 1) assert.equal(usersList[0].points, 2) }) + + test('updateOrCreateMany should work with DateTime', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + + const BaseModel = getBaseModel(adapter) + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @column() + declare email: string + + @column.dateTime() + declare createdAt: DateTime + } + + const createdAt1 = DateTime.now().minus({ days: 2 }).startOf('second') + const createdAt2 = DateTime.now().minus({ days: 1 }).startOf('second') + + await User.createMany([ + { + username: 'virk1', + email: 'virk+1@adonisjs.com', + createdAt: createdAt1, + }, + { + username: 'virk2', + email: 'virk+2@adonisjs.com', + createdAt: createdAt2, + }, + ]) + + const users = await User.updateOrCreateMany('createdAt', [ + { + username: 'virk3', + email: 'virk+3@adonisjs.com', + createdAt: createdAt1, + }, + { + username: 'nikk', + email: 'nikk@adonisjs.com', + createdAt: DateTime.now(), + }, + ]) + + assert.lengthOf(users, 2) + assert.isTrue(users[0].$isPersisted) + assert.isFalse(users[0].$isLocal) + + assert.isTrue(users[1].$isPersisted) + assert.isTrue(users[1].$isLocal) + + const usersList = await db.query().from('users') + assert.lengthOf(usersList, 3) + }) }) test.group('Base Model | hooks', (group) => { From 2a2bd22c2261f2548f4d794131b4b86fcdaea829 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 2 May 2024 12:17:15 +0530 Subject: [PATCH 46/73] chore(release): 20.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef861049..16b6d15d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/lucid", - "version": "20.5.1", + "version": "20.6.0", "description": "SQL ORM built on top of Active Record pattern", "engines": { "node": ">=18.16.0" From bdc32b5e585fca1eab9d875825e98c637be8d654 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 10 Jun 2024 21:42:29 +0530 Subject: [PATCH 47/73] chore: update dependencies --- package.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 16b6d15d..9a8c9cea 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "index:commands": "adonis-kit index build/commands" }, "dependencies": { - "@adonisjs/presets": "^2.4.0", + "@adonisjs/presets": "^2.4.1", "@faker-js/faker": "^8.4.1", "@poppinss/hooks": "^7.2.3", "@poppinss/macroable": "^1.0.2", @@ -74,24 +74,24 @@ "tarn": "^3.0.2" }, "devDependencies": { - "@adonisjs/assembler": "^7.5.0", - "@adonisjs/core": "^6.7.1", + "@adonisjs/assembler": "^7.7.0", + "@adonisjs/core": "^6.10.1", "@adonisjs/eslint-config": "^1.3.0", "@adonisjs/prettier-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", - "@commitlint/cli": "^19.2.2", + "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@japa/assert": "^3.0.0", "@japa/file-system": "^2.3.0", "@japa/runner": "^3.1.4", - "@swc/core": "^1.4.16", + "@swc/core": "^1.5.27", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.12.7", + "@types/node": "^20.14.2", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.15", - "@vinejs/vine": "^2.0.0", - "better-sqlite3": "^9.5.0", + "@vinejs/vine": "^2.1.0", + "better-sqlite3": "^11.0.0", "c8": "^9.1.0", "chance": "^1.1.11", "copyfiles": "^2.4.1", @@ -103,13 +103,13 @@ "github-label-sync": "^2.3.1", "husky": "^9.0.11", "luxon": "^3.4.4", - "mysql2": "^3.9.7", + "mysql2": "^3.10.0", "np": "^10.0.5", - "pg": "^8.11.5", - "prettier": "^3.2.5", + "pg": "^8.12.0", + "prettier": "^3.3.1", "reflect-metadata": "^0.2.2", "sqlite3": "^5.1.7", - "tedious": "^18.1.0", + "tedious": "^18.2.0", "ts-node": "^10.9.2", "typescript": "^5.4.5" }, From 9dbd7fcb06d0e73f228d4e878f725e916e3449b0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 10 Jun 2024 21:45:04 +0530 Subject: [PATCH 48/73] chore: update peer dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9a8c9cea..ccc0bdad 100644 --- a/package.json +++ b/package.json @@ -114,8 +114,8 @@ "typescript": "^5.4.5" }, "peerDependencies": { - "@adonisjs/assembler": "^7.0.0", - "@adonisjs/core": "^6.2.2", + "@adonisjs/assembler": "^7.7.0", + "@adonisjs/core": "^6.10.1", "luxon": "^3.4.4" }, "peerDependenciesMeta": { From 44da18cebab24885088877f5a7ef4f18aae7e2d4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 11 Jun 2024 11:01:17 +0530 Subject: [PATCH 49/73] test: fix broken types --- src/connection/index.ts | 5 +++-- src/connection/manager.ts | 28 ---------------------------- src/database/main.ts | 25 ------------------------- src/types/database.ts | 24 +++++------------------- test/orm/base_model.spec.ts | 12 ++++++------ 5 files changed, 14 insertions(+), 80 deletions(-) diff --git a/src/connection/index.ts b/src/connection/index.ts index 1e4688aa..04e6a87c 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -12,9 +12,10 @@ import knex, { Knex } from 'knex' import { EventEmitter } from 'node:events' import { patchKnex } from 'knex-dynamic-connection' import type { Logger } from '@adonisjs/core/logger' +import { HealthCheckResult } from '@adonisjs/core/types/health' // @ts-expect-error import { resolveClientNameWithAliases } from 'knex/lib/util/helpers.js' -import { ConnectionConfig, ConnectionContract, ReportNode } from '../types/database.js' +import { ConnectionConfig, ConnectionContract } from '../types/database.js' import { Logger as ConnectionLogger } from './logger.js' import * as errors from '../errors.js' @@ -392,7 +393,7 @@ export class Connection extends EventEmitter implements ConnectionContract { /** * Returns the healthcheck report for the connection */ - async getReport(): Promise { + async getReport(): Promise { const error = await this.checkWriteHost() let readError: Error | undefined diff --git a/src/connection/manager.ts b/src/connection/manager.ts index e6cfd020..5318e50d 100644 --- a/src/connection/manager.ts +++ b/src/connection/manager.ts @@ -11,7 +11,6 @@ import type { Emitter } from '@adonisjs/core/events' import type { Logger } from '@adonisjs/core/logger' import { - ReportNode, ConnectionNode, ConnectionConfig, ConnectionContract, @@ -244,31 +243,4 @@ export class ConnectionManager implements ConnectionManagerContract { this.connections.delete(connectionName) } } - - /** - * Returns the report for all the connections marked for healthChecks. - */ - async report(): Promise { - const reports = await Promise.all( - Array.from(this.connections.keys()) - .filter((one) => this.get(one)!.config.healthCheck) - .map((one) => { - this.connect(one) - return this.get(one)!.connection!.getReport() - }) - ) - - const healthy = !reports.find((report) => !!report.error) - - return { - displayName: 'Database', - health: { - healthy, - message: healthy - ? 'All connections are healthy' - : 'One or more connections are not healthy', - }, - meta: reports, - } - } } diff --git a/src/database/main.ts b/src/database/main.ts index bfc40548..3ea6b920 100644 --- a/src/database/main.ts +++ b/src/database/main.ts @@ -53,7 +53,6 @@ export class Database extends Macroable { * A store of global transactions */ connectionGlobalTransactions: Map = new Map() - hasHealthChecksEnabled = false prettyPrint = prettyPrint constructor( @@ -66,23 +65,6 @@ export class Database extends Macroable { this.primaryConnectionName = this.config.connection this.registerConnections() - this.findIfHealthChecksAreEnabled() - } - - /** - * Compute whether health check is enabled or not after registering the connections. - * There are chances that all pre-registered connections are not using health - * checks but a dynamic connection is using it. We don't support that use case - * for now, since it complicates things a lot and forces us to register the - * health checker on demand. - */ - private findIfHealthChecksAreEnabled() { - for (let [, conn] of this.manager.connections) { - if (conn.config.healthCheck) { - this.hasHealthChecksEnabled = true - break - } - } } /** @@ -254,13 +236,6 @@ export class Database extends Macroable { : client.transaction(callbackOrOptions) } - /** - * Invokes `manager.report` - */ - report() { - return this.manager.report() - } - /** * Begin a new global transaction */ diff --git a/src/types/database.ts b/src/types/database.ts index 7faf184d..8fd837bf 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -12,15 +12,16 @@ import type { Pool } from 'tarn' import type { EventEmitter } from 'node:events' import type { ConnectionOptions } from 'node:tls' import type { Emitter } from '@adonisjs/core/events' +import type { HealthCheckResult } from '@adonisjs/core/types/health' import { LucidModel, ModelQueryBuilderContract } from './model.js' import { - DatabaseQueryBuilderContract, FromTable, - InsertQueryBuilderContract, - RawBuilderContract, RawQueryBindings, + RawBuilderContract, RawQueryBuilderContract, ReferenceBuilderContract, + InsertQueryBuilderContract, + DatabaseQueryBuilderContract, } from './querybuilder.js' /** @@ -309,15 +310,6 @@ type SharedConnectionNode = { port?: number } -/** - * Shape of the report node for the database connection report - */ -export type ReportNode = { - connection: string - message: string - error: any -} - /** * Migrations config */ @@ -344,7 +336,6 @@ export type SharedConfigNode = { debug?: boolean asyncStackTraces?: boolean revision?: number - healthCheck?: boolean migrations?: MigratorConfig seeders?: SeedersConfig wipe?: { ignoreTables?: string[] } @@ -638,11 +629,6 @@ export interface ConnectionManagerContract { * re-add it using the `add` method */ release(connectionName: string): Promise - - /** - * Returns the health check report for registered connections - */ - report(): Promise } /** @@ -712,7 +698,7 @@ export interface ConnectionContract extends EventEmitter { /** * Returns the connection report */ - getReport(): Promise + getReport(): Promise } /** diff --git a/test/orm/base_model.spec.ts b/test/orm/base_model.spec.ts index 5079910e..1feddbbb 100644 --- a/test/orm/base_model.spec.ts +++ b/test/orm/base_model.spec.ts @@ -6029,7 +6029,7 @@ test.group('Base Model | date', (group) => { User.$adapter = adapter adapter.on('insert', (model: LucidRow, _: any) => { - assert.instanceOf((model as User).dob, DateTime) + assert.instanceOf((model as User).dob, DateTime as any) }) user.username = 'virk' @@ -6066,7 +6066,7 @@ test.group('Base Model | date', (group) => { User.$adapter = adapter adapter.on('insert', (model: LucidRow, _: any) => { - assert.instanceOf((model as User).dob, DateTime) + assert.instanceOf((model as User).dob, DateTime as any) assert.isUndefined((model as User).createdAt) }) @@ -6097,7 +6097,7 @@ test.group('Base Model | date', (group) => { const user = new User() User.$adapter = adapter adapter.on('update', (model: LucidRow) => { - assert.instanceOf((model as User).updatedAt, DateTime) + assert.instanceOf((model as User).updatedAt, DateTime as any) }) user.username = 'virk' @@ -6320,7 +6320,7 @@ test.group('Base Model | date', (group) => { await db.insertQuery().table('users').insert({ username: 'virk' }) const user = await User.find(1) - assert.instanceOf(user!.createdAt, DateTime) + assert.instanceOf(user!.createdAt, DateTime as any) }) test('ignore null or empty values during fetch', async ({ fs, assert }) => { @@ -6520,7 +6520,7 @@ test.group('Base Model | datetime', (group) => { const user = new User() user.username = 'virk' await user.save() - assert.instanceOf(user.joinedAt, DateTime) + assert.instanceOf(user.joinedAt, DateTime as any) const createdUser = await db.from('users').select('*').first() @@ -6688,7 +6688,7 @@ test.group('Base Model | datetime', (group) => { await db.insertQuery().table('users').insert({ username: 'virk' }) const user = await User.find(1) - assert.instanceOf(user!.createdAt, DateTime) + assert.instanceOf(user!.createdAt, DateTime as any) }) test('ignore null or empty values during fetch', async ({ fs, assert }) => { From 32f377e8a42c78fa4ea6424f8ca31798deb15837 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 11 Jun 2024 11:19:16 +0530 Subject: [PATCH 50/73] refactor: remove legacy health checks --- src/connection/index.ts | 22 ---------- test/connection/connection.spec.ts | 48 -------------------- test/connection/connection_manager.spec.ts | 51 ---------------------- test/database/database.spec.ts | 13 ------ test/orm/model_many_to_many.spec.ts | 12 ++--- 5 files changed, 6 insertions(+), 140 deletions(-) diff --git a/src/connection/index.ts b/src/connection/index.ts index 04e6a87c..e19ec13c 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -389,26 +389,4 @@ export class Connection extends EventEmitter implements ConnectionContract { } } } - - /** - * Returns the healthcheck report for the connection - */ - async getReport(): Promise { - const error = await this.checkWriteHost() - let readError: Error | undefined - - if (!error && this.hasReadWriteReplicas) { - readError = await this.checkReadHosts() - } - - return { - connection: this.name, - message: readError - ? 'Unable to reach one of the read hosts' - : error - ? 'Unable to reach the database server' - : 'Connection is healthy', - error: error || readError || null, - } - } } diff --git a/test/connection/connection.spec.ts b/test/connection/connection.spec.ts index 7d42593d..274fc94d 100644 --- a/test/connection/connection.spec.ts +++ b/test/connection/connection.spec.ts @@ -187,52 +187,4 @@ test.group('Health Checks', (group) => { await connection.disconnect() }) - - if (!['sqlite', 'better_sqlite'].includes(process.env.DB!)) { - test('get healthcheck report for un-healthy connection', async ({ assert }) => { - const connection = new Connection( - 'primary', - Object.assign({}, getConfig(), { - connection: { - host: 'bad-host', - }, - }), - logger - ) - connection.connect() - - const report = await connection.getReport() - assert.equal(report.message, 'Unable to reach the database server') - assert.exists(report.error) - - await connection.disconnect() - }).timeout(0) - - test('get healthcheck report for un-healthy read host', async ({ assert }) => { - const connection = new Connection( - 'primary', - Object.assign({}, getConfig(), { - replicas: { - write: { - connection: getConfig().connection, - }, - read: { - connection: [ - getConfig().connection, - Object.assign({}, getConfig().connection, { host: 'bad-host', port: 8000 }), - ], - }, - }, - }), - logger - ) - connection.connect() - - const report = await connection.getReport() - assert.equal(report.message, 'Unable to reach one of the read hosts') - assert.exists(report.error) - - await connection.disconnect() - }).timeout(0) - } }) diff --git a/test/connection/connection_manager.spec.ts b/test/connection/connection_manager.spec.ts index 72488aad..b26efd84 100644 --- a/test/connection/connection_manager.spec.ts +++ b/test/connection/connection_manager.spec.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ -import { join } from 'node:path' import { test } from '@japa/runner' import { Connection } from '../../src/connection/index.js' @@ -19,7 +18,6 @@ import { mapToObj, logger, createEmitter, - SQLITE_BASE_PATH, } from '../../test-helpers/index.js' test.group('ConnectionManager', (group) => { @@ -188,53 +186,4 @@ test.group('ConnectionManager', (group) => { manager.patch('primary', getConfig()) manager.connect('primary') }).waitForDone() - - test('get health check report for connections that has enabled health checks', async ({ - assert, - }) => { - const manager = new ConnectionManager(logger, createEmitter()) - manager.add('primary', Object.assign({}, getConfig(), { healthCheck: true })) - manager.add('secondary', Object.assign({}, getConfig(), { healthCheck: true })) - manager.add('secondary-copy', Object.assign({}, getConfig(), { healthCheck: false })) - - const report = await manager.report() - assert.equal(report.health.healthy, true) - assert.equal(report.health.message, 'All connections are healthy') - assert.lengthOf(report.meta, 2) - assert.deepEqual( - report.meta.map((node: any) => node.connection), - ['primary', 'secondary'] - ) - - await manager.closeAll() - }) - - test('get health check report when one of the connection is unhealthy', async ({ assert }) => { - const manager = new ConnectionManager(logger, createEmitter()) - manager.add('primary', Object.assign({}, getConfig(), { healthCheck: true })) - manager.add( - 'secondary', - Object.assign({}, getConfig(), { - healthCheck: true, - connection: ['sqlite', 'better_sqlite'].includes(process.env.DB!) - ? { - filename: join(SQLITE_BASE_PATH, 'nested', 'db.sqlite'), - } - : { - host: 'bad-host', - }, - }) - ) - manager.add('secondary-copy', Object.assign({}, getConfig(), { healthCheck: false })) - - const report = await manager.report() - assert.equal(report.health.healthy, false) - assert.equal(report.health.message, 'One or more connections are not healthy') - assert.lengthOf(report.meta, 2) - assert.deepEqual( - report.meta.map((node: any) => node.connection), - ['primary', 'secondary'] - ) - await manager.closeAll() - }).timeout(0) }) diff --git a/test/database/database.spec.ts b/test/database/database.spec.ts index b3f8084f..d37fe409 100644 --- a/test/database/database.spec.ts +++ b/test/database/database.spec.ts @@ -153,19 +153,6 @@ test.group('Database', (group) => { assert.isDefined(result) await db.manager.closeAll() }) - - test('set hasHealthChecks enabled flag to true, when one ore more connections are using health checks', async ({ - assert, - }) => { - const config = { - connection: 'primary', - connections: { primary: Object.assign({}, getConfig(), { healthCheck: true }) }, - } - - const db = new Database(config, logger, createEmitter()) - assert.isTrue(db.hasHealthChecksEnabled) - await db.manager.closeAll() - }) }) test.group('Database | global transaction', (group) => { diff --git a/test/orm/model_many_to_many.spec.ts b/test/orm/model_many_to_many.spec.ts index bb26c522..c1444f57 100644 --- a/test/orm/model_many_to_many.spec.ts +++ b/test/orm/model_many_to_many.spec.ts @@ -775,8 +775,8 @@ test.group('Model | ManyToMany | bulk operations', (group) => { assert.equal(skills[0].name, 'Programming') assert.equal(skills[0].$extras.pivot_user_id, 1) assert.equal(skills[0].$extras.pivot_skill_id, 1) - assert.instanceOf(skills[0].$extras.pivot_created_at, DateTime) - assert.instanceOf(skills[0].$extras.pivot_updated_at, DateTime) + assert.instanceOf(skills[0].$extras.pivot_created_at, DateTime as any) + assert.instanceOf(skills[0].$extras.pivot_updated_at, DateTime as any) }) }) @@ -1337,8 +1337,8 @@ test.group('Model | ManyToMany | preload', (group) => { assert.equal(users[0].skills[0].name, 'Programming') assert.equal(users[0].skills[0].$extras.pivot_user_id, 1) assert.equal(users[0].skills[0].$extras.pivot_skill_id, 1) - assert.instanceOf(users[0].skills[0].$extras.pivot_created_at, DateTime) - assert.instanceOf(users[0].skills[0].$extras.pivot_updated_at, DateTime) + assert.instanceOf(users[0].skills[0].$extras.pivot_created_at, DateTime as any) + assert.instanceOf(users[0].skills[0].$extras.pivot_updated_at, DateTime as any) }) test('preload relation for many', async ({ fs, assert }) => { @@ -1540,8 +1540,8 @@ test.group('Model | ManyToMany | preload', (group) => { assert.equal(users[0].skills[0].name, 'Programming') assert.equal(users[0].skills[0].$extras.pivot_user_id, 1) assert.equal(users[0].skills[0].$extras.pivot_skill_id, 1) - assert.instanceOf(users[0].skills[0].$extras.pivot_created_at, DateTime) - assert.instanceOf(users[0].skills[0].$extras.pivot_updated_at, DateTime) + assert.instanceOf(users[0].skills[0].$extras.pivot_created_at, DateTime as any) + assert.instanceOf(users[0].skills[0].$extras.pivot_updated_at, DateTime as any) }) test('select extra pivot columns', async ({ fs, assert }) => { From 0373e7f97d7ecb584d360d87a5d546b1f56f6541 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 18 Jun 2024 12:55:56 +0530 Subject: [PATCH 51/73] feat: add database health checks and remove legacy health check flag Breaking: The config.healthCheck is no longer relevant --- package.json | 60 +++--- src/connection/index.ts | 63 ------- src/database/checks/db_check.ts | 71 ++++++++ .../checks/db_connection_count_check.ts | 171 ++++++++++++++++++ src/types/database.ts | 6 - test/connection/connection.spec.ts | 25 --- test/database/db_check.spec.ts | 71 ++++++++ .../db_connection_count_check.spec.ts | 164 +++++++++++++++++ 8 files changed, 507 insertions(+), 124 deletions(-) create mode 100644 src/database/checks/db_check.ts create mode 100644 src/database/checks/db_connection_count_check.ts create mode 100644 test/database/db_check.spec.ts create mode 100644 test/database/db_connection_count_check.spec.ts diff --git a/package.json b/package.json index ccc0bdad..be8da19b 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "@adonisjs/lucid", - "version": "20.6.0", "description": "SQL ORM built on top of Active Record pattern", + "version": "20.6.0", "engines": { "node": ">=18.16.0" }, "main": "./build/index.js", + "type": "module", "files": [ "build/commands", "build/providers", @@ -17,7 +18,6 @@ "build/configure.d.ts", "build/configure.js" ], - "type": "module", "exports": { ".": "./build/index.js", "./schema": "./build/src/schema/main.js", @@ -57,25 +57,9 @@ "test": "c8 npm run test:docker", "index:commands": "adonis-kit index build/commands" }, - "dependencies": { - "@adonisjs/presets": "^2.4.1", - "@faker-js/faker": "^8.4.1", - "@poppinss/hooks": "^7.2.3", - "@poppinss/macroable": "^1.0.2", - "@poppinss/utils": "^6.7.3", - "fast-deep-equal": "^3.1.3", - "igniculus": "^1.5.0", - "kleur": "^4.1.5", - "knex": "^3.1.0", - "knex-dynamic-connection": "^3.1.1", - "pretty-hrtime": "^1.0.3", - "qs": "^6.12.1", - "slash": "^5.1.0", - "tarn": "^3.0.2" - }, "devDependencies": { "@adonisjs/assembler": "^7.7.0", - "@adonisjs/core": "^6.10.1", + "@adonisjs/core": "^6.11.0", "@adonisjs/eslint-config": "^1.3.0", "@adonisjs/prettier-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", @@ -84,15 +68,15 @@ "@japa/assert": "^3.0.0", "@japa/file-system": "^2.3.0", "@japa/runner": "^3.1.4", - "@swc/core": "^1.5.27", + "@swc/core": "^1.6.1", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.2", + "@types/node": "^20.14.4", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.15", "@vinejs/vine": "^2.1.0", "better-sqlite3": "^11.0.0", - "c8": "^9.1.0", + "c8": "^10.1.2", "chance": "^1.1.11", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -103,16 +87,32 @@ "github-label-sync": "^2.3.1", "husky": "^9.0.11", "luxon": "^3.4.4", - "mysql2": "^3.10.0", + "mysql2": "^3.10.1", "np": "^10.0.5", "pg": "^8.12.0", - "prettier": "^3.3.1", + "prettier": "^3.3.2", "reflect-metadata": "^0.2.2", "sqlite3": "^5.1.7", "tedious": "^18.2.0", "ts-node": "^10.9.2", "typescript": "^5.4.5" }, + "dependencies": { + "@adonisjs/presets": "^2.4.1", + "@faker-js/faker": "^8.4.1", + "@poppinss/hooks": "^7.2.3", + "@poppinss/macroable": "^1.0.2", + "@poppinss/utils": "^6.7.3", + "fast-deep-equal": "^3.1.3", + "igniculus": "^1.5.0", + "kleur": "^4.1.5", + "knex": "^3.1.0", + "knex-dynamic-connection": "^3.2.0", + "pretty-hrtime": "^1.0.3", + "qs": "^6.12.1", + "slash": "^5.1.0", + "tarn": "^3.0.2" + }, "peerDependencies": { "@adonisjs/assembler": "^7.7.0", "@adonisjs/core": "^6.10.1", @@ -126,8 +126,8 @@ "optional": true } }, - "license": "MIT", "author": "virk,adonisjs", + "license": "MIT", "homepage": "https://github.com/adonisjs/lucid#readme", "repository": { "type": "git", @@ -140,6 +140,11 @@ "extends": "@adonisjs/eslint-config/package" }, "prettier": "@adonisjs/prettier-config", + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, "publishConfig": { "access": "public", "tag": "latest" @@ -150,11 +155,6 @@ "branch": "main", "anyBranch": false }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] - }, "c8": { "reporter": [ "text", diff --git a/src/connection/index.ts b/src/connection/index.ts index e19ec13c..9d382582 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -12,7 +12,6 @@ import knex, { Knex } from 'knex' import { EventEmitter } from 'node:events' import { patchKnex } from 'knex-dynamic-connection' import type { Logger } from '@adonisjs/core/logger' -import { HealthCheckResult } from '@adonisjs/core/types/health' // @ts-expect-error import { resolveClientNameWithAliases } from 'knex/lib/util/helpers.js' import { ConnectionConfig, ConnectionContract } from '../types/database.js' @@ -256,68 +255,6 @@ export class Connection extends EventEmitter implements ConnectionContract { patchKnex(this.readClient, this.readConfigResolver.bind(this)) } - /** - * Checks all the read hosts by running a query on them. Stops - * after first error. - */ - private async checkReadHosts() { - const configCopy = Object.assign( - { log: new ConnectionLogger(this.name, this.logger) }, - this.config, - { - debug: false, - } - ) - let error: any = null - - // eslint-disable-next-line @typescript-eslint/naming-convention - for (let _ of this.readReplicas) { - configCopy.connection = this.readConfigResolver(this.config) - this.logger.trace({ connection: this.name }, 'spawing health check read connection') - const client = knex.knex(configCopy) - - try { - if (this.dialectName === 'oracledb') { - await client.raw('SELECT 1 + 1 AS result FROM dual') - } else { - await client.raw('SELECT 1 + 1 AS result') - } - } catch (err) { - error = err - } - - /** - * Cleanup client connection - */ - await client.destroy() - this.logger.trace({ connection: this.name }, 'destroying health check read connection') - - /** - * Return early when there is an error - */ - if (error) { - break - } - } - - return error - } - - /** - * Checks for the write host - */ - private async checkWriteHost() { - try { - if (this.dialectName === 'oracledb') { - await this.client!.raw('SELECT 1 + 1 AS result FROM dual') - } else { - await this.client!.raw('SELECT 1 + 1 AS result') - } - } catch (error) { - return error - } - } - /** * Returns the pool instance for the given connection */ diff --git a/src/database/checks/db_check.ts b/src/database/checks/db_check.ts new file mode 100644 index 00000000..50282602 --- /dev/null +++ b/src/database/checks/db_check.ts @@ -0,0 +1,71 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCheck, Result } from '@adonisjs/core/health' +import type { HealthCheckResult } from '@adonisjs/core/types/health' +import type { QueryClientContract } from '../../types/database.js' + +/** + * The DbCheck attempts to establish the database connection by + * executing a sample query. + */ +export class DbCheck extends BaseCheck { + #client: QueryClientContract + + /** + * Health check public name + */ + name: string + + constructor(client: QueryClientContract) { + super() + this.#client = client + this.name = `Database health check (${client.connectionName})` + } + + /** + * Returns connection metadata to be shared in the health checks + * report + */ + #getConnectionMetadata() { + return { + connection: { + name: this.#client.connectionName, + dialect: this.#client.dialect.name, + }, + } + } + + /** + * Internal method to ping the database server + */ + async #ping() { + if (this.#client.dialect.name === 'oracledb') { + await this.#client.rawQuery('SELECT 1 + 1 AS result FROM dual') + } else { + await this.#client!.rawQuery('SELECT 1 + 1 AS result') + } + } + + /** + * Executes the health check + */ + async run(): Promise { + try { + await this.#ping() + return Result.ok('Successfully connected to the database server').mergeMetaData( + this.#getConnectionMetadata() + ) + } catch (error) { + return Result.failed(error.message || 'Connection failed', error).mergeMetaData( + this.#getConnectionMetadata() + ) + } + } +} diff --git a/src/database/checks/db_connection_count_check.ts b/src/database/checks/db_connection_count_check.ts new file mode 100644 index 00000000..d7f590ac --- /dev/null +++ b/src/database/checks/db_connection_count_check.ts @@ -0,0 +1,171 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCheck, Result } from '@adonisjs/core/health' +import type { HealthCheckResult } from '@adonisjs/core/types/health' +import type { QueryClientContract } from '../../types/database.js' + +/** + * The DbConnectionCountCheck can be used to monitor the active + * database connections and report a warning or error after + * a certain threshold has been execeeded. + */ +export class DbConnectionCountCheck extends BaseCheck { + #client: QueryClientContract + + /** + * Method to compute the memory consumption + */ + #computeFn: (client: QueryClientContract) => Promise = async (client) => { + if (client.dialect.name === 'postgres') { + const response = await client.query().from('pg_stat_activity').count('* as connections') + return Number(response[0].connections) + } + + if (client.dialect.name === 'mysql') { + const response = await client + .query() + .from('information_schema.PROCESSLIST') + .count('* as connections') + return Number(response[0].connections) + } + + return null + } + + /** + * Connections count threshold after which a warning will be created + */ + #warnThreshold: number = 10 + + /** + * Connections count threshold after which an error will be created + */ + #failThreshold: number = 15 + + /** + * Health check public name + */ + name: string + + constructor(client: QueryClientContract) { + super() + this.#client = client + this.name = `Connection count health check (${client.connectionName})` + } + + /** + * Returns connection metadata to be shared in the health checks + * report + */ + #getConnectionMetadata() { + return { + connection: { + name: this.#client.connectionName, + dialect: this.#client.dialect.name, + }, + } + } + + /** + * Returns connections count metadata to be shared in the + * health checks report + */ + #getConnectionsCountMetadata(active: number) { + return { + connectionsCount: { + active, + warningThreshold: this.#warnThreshold, + failureThreshold: this.#failThreshold, + }, + } + } + + /** + * Define the connections count threshold after which a + * warning should be created. + * + * ``` + * .warnWhenExceeds(20) + * ``` + */ + warnWhenExceeds(connectionsCount: number) { + this.#warnThreshold = connectionsCount + return this + } + + /** + * Define the connections count threshold after which an + * error should be created. + * + * ``` + * .failWhenExceeds(30) + * ``` + */ + failWhenExceeds(connectionsCount: number) { + this.#failThreshold = connectionsCount + return this + } + + /** + * Define a custom callback to compute database connections count. + * The return value must be a number of active connections + * or null (if dialect is not supported). + */ + compute(callback: (client: QueryClientContract) => Promise): this { + this.#computeFn = callback + return this + } + + /** + * Executes the health check + */ + async run(): Promise { + try { + const connectionsCount = await this.#computeFn(this.#client) + if (!connectionsCount) { + return Result.ok( + `Check skipped. Unable to get active connections for ${this.#client.dialect.name} dialect` + ).mergeMetaData(this.#getConnectionMetadata()) + } + + /** + * Check if we have crossed the failure threshold + */ + if (connectionsCount > this.#failThreshold) { + return Result.failed( + `There are ${connectionsCount} active connections, which is above the threshold of ${this.#failThreshold} connections` + ) + .mergeMetaData(this.#getConnectionMetadata()) + .mergeMetaData(this.#getConnectionsCountMetadata(connectionsCount)) + } + + /** + * Check if we have crossed the warning threshold + */ + if (connectionsCount > this.#warnThreshold) { + return Result.warning( + `There are ${connectionsCount} active connections, which is above the threshold of ${this.#warnThreshold} connections` + ) + .mergeMetaData(this.#getConnectionMetadata()) + .mergeMetaData(this.#getConnectionsCountMetadata(connectionsCount)) + } + + return Result.ok( + `There are ${connectionsCount} active connections, which is under the defined thresholds` + ) + .mergeMetaData(this.#getConnectionMetadata()) + .mergeMetaData(this.#getConnectionsCountMetadata(connectionsCount)) + } catch (error) { + return Result.failed(error.message || 'Connection failed', error).mergeMetaData( + this.#getConnectionMetadata() + ) + } + } +} diff --git a/src/types/database.ts b/src/types/database.ts index 8fd837bf..b46b277a 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -12,7 +12,6 @@ import type { Pool } from 'tarn' import type { EventEmitter } from 'node:events' import type { ConnectionOptions } from 'node:tls' import type { Emitter } from '@adonisjs/core/events' -import type { HealthCheckResult } from '@adonisjs/core/types/health' import { LucidModel, ModelQueryBuilderContract } from './model.js' import { FromTable, @@ -694,11 +693,6 @@ export interface ConnectionContract extends EventEmitter { * Disconnect knex */ disconnect(): Promise - - /** - * Returns the connection report - */ - getReport(): Promise } /** diff --git a/test/connection/connection.spec.ts b/test/connection/connection.spec.ts index 274fc94d..1b709e78 100644 --- a/test/connection/connection.spec.ts +++ b/test/connection/connection.spec.ts @@ -10,7 +10,6 @@ import { Knex } from 'knex' import { test } from '@japa/runner' import { MysqlConfig } from '../../src/types/database.js' - import { Connection } from '../../src/connection/index.js' import { setup, cleanup, getConfig, resetTables, logger } from '../../test-helpers/index.js' @@ -164,27 +163,3 @@ if (process.env.DB === 'mysql') { }) }) } - -test.group('Health Checks', (group) => { - group.setup(async () => { - await setup() - }) - - group.teardown(async () => { - await cleanup() - }) - - test('get healthcheck report for healthy connection', async ({ assert }) => { - const connection = new Connection('primary', getConfig(), logger) - connection.connect() - - const report = await connection.getReport() - assert.deepEqual(report, { - connection: 'primary', - message: 'Connection is healthy', - error: null, - }) - - await connection.disconnect() - }) -}) diff --git a/test/database/db_check.spec.ts b/test/database/db_check.spec.ts new file mode 100644 index 00000000..e1369b44 --- /dev/null +++ b/test/database/db_check.spec.ts @@ -0,0 +1,71 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Database } from '../../src/database/main.js' +import { DbCheck } from '../../src/database/checks/db_check.js' +import { getConfig, setup, cleanup, logger, createEmitter } from '../../test-helpers/index.js' + +test.group('Db connection check', (group) => { + group.setup(async () => { + await setup() + }) + + group.teardown(async () => { + await cleanup() + }) + + test('perform health check for a connection', async ({ assert }) => { + const config = { + connection: 'primary', + connections: { primary: getConfig() }, + } + + const db = new Database(config, logger, createEmitter()) + + const healthCheck = new DbCheck(db.connection()) + const result = await healthCheck.run() + assert.containsSubset(result, { + message: 'Successfully connected to the database server', + status: 'ok', + meta: { connection: { name: 'primary', dialect: config.connections.primary.client } }, + }) + + await db.manager.closeAll() + }) + + test('report error when unable to connect', async ({ assert }) => { + const config = { + connection: 'primary', + connections: { + primary: { + client: 'mysql2' as const, + connection: { + host: 'localhost', + port: 3333, + }, + }, + }, + } + + const db = new Database(config, logger, createEmitter()) + + const healthCheck = new DbCheck(db.connection()) + const result = await healthCheck.run() + assert.containsSubset(result, { + message: 'Connection failed', + status: 'error', + meta: { connection: { name: 'primary', dialect: 'mysql' } }, + }) + assert.equal(result.meta?.error.code, 'ECONNREFUSED') + + await db.manager.closeAll() + }) +}) diff --git a/test/database/db_connection_count_check.spec.ts b/test/database/db_connection_count_check.spec.ts new file mode 100644 index 00000000..32e30939 --- /dev/null +++ b/test/database/db_connection_count_check.spec.ts @@ -0,0 +1,164 @@ +/* + * @adonisjs/lucid + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { Database } from '../../src/database/main.js' +import { getConfig, setup, cleanup, logger, createEmitter } from '../../test-helpers/index.js' +import { DbConnectionCountCheck } from '../../src/database/checks/db_connection_count_check.js' + +test.group('Db connection count check', (group) => { + group.setup(async () => { + await setup() + }) + + group.teardown(async () => { + await cleanup() + }) + + test('return error when failure threshold has been crossed', async ({ assert }) => { + const config = { + connection: 'primary', + connections: { primary: getConfig() }, + } + + const db = new Database(config, logger, createEmitter()) + + const healthCheck = new DbConnectionCountCheck(db.connection()).compute(async () => { + return 20 + }) + + const result = await healthCheck.run() + assert.containsSubset(result, { + message: 'There are 20 active connections, which is above the threshold of 15 connections', + status: 'error', + meta: { + connection: { name: 'primary', dialect: config.connections.primary.client }, + connectionsCount: { + active: 20, + failureThreshold: 15, + warningThreshold: 10, + }, + }, + }) + + await db.manager.closeAll() + }) + + test('return warning when warning threshold has been crossed', async ({ assert }) => { + const config = { + connection: 'primary', + connections: { primary: getConfig() }, + } + + const db = new Database(config, logger, createEmitter()) + + const healthCheck = new DbConnectionCountCheck(db.connection()).compute(async () => { + return 12 + }) + + const result = await healthCheck.run() + assert.containsSubset(result, { + message: 'There are 12 active connections, which is above the threshold of 10 connections', + status: 'warning', + meta: { + connection: { name: 'primary', dialect: config.connections.primary.client }, + connectionsCount: { + active: 12, + failureThreshold: 15, + warningThreshold: 10, + }, + }, + }) + + await db.manager.closeAll() + }) + + test('return success when unable to compute connections count', async ({ assert }) => { + const config = { + connection: 'primary', + connections: { primary: getConfig() }, + } + + const db = new Database(config, logger, createEmitter()) + + const healthCheck = new DbConnectionCountCheck(db.connection()).compute(async () => { + return null + }) + + const result = await healthCheck.run() + assert.containsSubset(result, { + message: `Check skipped. Unable to get active connections for ${config.connections.primary.client} dialect`, + status: 'ok', + meta: { + connection: { name: 'primary', dialect: config.connections.primary.client }, + }, + }) + + await db.manager.closeAll() + }) + + test('get PostgreSQL connections count', async ({ assert }) => { + const config = { + connection: 'primary', + connections: { primary: getConfig() }, + } + + const db = new Database(config, logger, createEmitter()) + + const healthCheck = new DbConnectionCountCheck(db.connection()) + + const result = await healthCheck.run() + const activeConnections = result.meta?.connectionsCount.active + + assert.containsSubset(result, { + message: `There are ${activeConnections} active connections, which is under the defined thresholds`, + status: 'ok', + meta: { + connection: { name: 'primary', dialect: db.connection().dialect.name }, + connectionsCount: { + active: activeConnections, + failureThreshold: 15, + warningThreshold: 10, + }, + }, + }) + + await db.manager.closeAll() + }).skip(process.env.DB !== 'pg', 'Only for PostgreSQL') + + test('get MySQL connections count', async ({ assert }) => { + const config = { + connection: 'primary', + connections: { primary: getConfig() }, + } + + const db = new Database(config, logger, createEmitter()) + + const healthCheck = new DbConnectionCountCheck(db.connection()) + + const result = await healthCheck.run() + const activeConnections = result.meta?.connectionsCount.active + + assert.containsSubset(result, { + message: `There are ${activeConnections} active connections, which is under the defined thresholds`, + status: 'ok', + meta: { + connection: { name: 'primary', dialect: db.connection().dialect.name }, + connectionsCount: { + active: activeConnections, + failureThreshold: 15, + warningThreshold: 10, + }, + }, + }) + + await db.manager.closeAll() + }).skip(!['mysql', 'mysql_legacy'].includes(process.env.DB!), 'Only for MySQL') +}) From 1b32dca50e374ead3bb9e7d51c62c26ff6c2d838 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 18 Jun 2024 12:57:53 +0530 Subject: [PATCH 52/73] feat: export health check classes --- src/database/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/database/main.ts b/src/database/main.ts index 3ea6b920..272abf38 100644 --- a/src/database/main.ts +++ b/src/database/main.ts @@ -32,6 +32,8 @@ import { ReferenceBuilder } from './static_builder/reference.js' import { SimplePaginator } from './paginator/simple_paginator.js' import { DatabaseQueryBuilder } from './query_builder/database.js' +export { DbCheck } from './checks/db_check.js' +export { DbConnectionCountCheck } from './checks/db_connection_count_check.js' export { DatabaseQueryBuilder, InsertQueryBuilder, SimplePaginator, QueryClient } /** From 95f11d1c5a3d912b74fa2046ecf85289ff1c7a89 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 18 Jun 2024 14:46:19 +0530 Subject: [PATCH 53/73] feat: add support for libsql and cleanup clients to dialects mapping Deprecates: connection.dialectName in favor of connection.clientName --- package.json | 2 + src/clients/libsql.cjs | 14 +++++++ src/connection/index.ts | 35 +++++++++++++++-- src/dialects/base_sqlite.ts | 2 +- src/dialects/index.ts | 17 ++++++-- src/dialects/libsql.ts | 15 +++++++ src/errors.ts | 9 +++++ src/query_client/index.ts | 9 +++-- src/query_runner/index.ts | 2 +- src/types/database.ts | 37 ++++++++++++++++++ test-helpers/index.ts | 34 ++++++++++++++-- test-helpers/tmp/libsql.db | Bin 0 -> 102400 bytes test/connection/connection.spec.ts | 24 +----------- test/connection/connection_manager.spec.ts | 25 +----------- .../db_connection_count_check.spec.ts | 25 +++++++----- test/database/drop_tables.spec.ts | 2 +- test/database/query_builder.spec.ts | 4 +- test/database/query_client.spec.ts | 29 ++++++-------- test/database/views_types.spec.ts | 2 +- test/migrations/schema.spec.ts | 9 ++--- 20 files changed, 196 insertions(+), 100 deletions(-) create mode 100644 src/clients/libsql.cjs create mode 100644 src/dialects/libsql.ts create mode 100644 test-helpers/tmp/libsql.db diff --git a/package.json b/package.json index be8da19b..b401a622 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "scripts": { "pretest": "npm run lint", "test:better_sqlite": "cross-env DB=better_sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", + "test:libsql": "cross-env DB=libsql node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "test:sqlite": "cross-env DB=sqlite node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "test:mysql": "cross-env DB=mysql node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "test:mysql_legacy": "cross-env DB=mysql_legacy node --enable-source-maps --loader=ts-node/esm ./bin/test.js", @@ -68,6 +69,7 @@ "@japa/assert": "^3.0.0", "@japa/file-system": "^2.3.0", "@japa/runner": "^3.1.4", + "@libsql/sqlite3": "^0.3.1", "@swc/core": "^1.6.1", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", diff --git a/src/clients/libsql.cjs b/src/clients/libsql.cjs new file mode 100644 index 00000000..19d6dca6 --- /dev/null +++ b/src/clients/libsql.cjs @@ -0,0 +1,14 @@ +const Sqlite3Client = require('knex/lib/dialects/sqlite3') + +class LibSQLClient extends Sqlite3Client { + _driver() { + return require('@libsql/sqlite3') + } +} + +Object.assign(LibSQLClient.prototype, { + dialect: 'libsql', + driverName: 'libsql', +}) + +module.exports = LibSQLClient diff --git a/src/connection/index.ts b/src/connection/index.ts index 9d382582..f6d731c1 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -14,10 +14,13 @@ import { patchKnex } from 'knex-dynamic-connection' import type { Logger } from '@adonisjs/core/logger' // @ts-expect-error import { resolveClientNameWithAliases } from 'knex/lib/util/helpers.js' -import { ConnectionConfig, ConnectionContract } from '../types/database.js' -import { Logger as ConnectionLogger } from './logger.js' import * as errors from '../errors.js' +import { clientsNames } from '../dialects/index.js' +// @ts-expect-error +import LibSQLClient from '../clients/libsql.cjs' +import { Logger as ConnectionLogger } from './logger.js' +import type { ConnectionConfig, ConnectionContract } from '../types/database.js' /** * Connection class manages a given database connection. Internally it uses @@ -38,10 +41,17 @@ export class Connection extends EventEmitter implements ConnectionContract { readClient?: Knex /** - * Connection dialect name + * Connection dialect name. + * @deprecated + * @see clientName */ dialectName: ConnectionContract['dialectName'] + /** + * Connection client name. + */ + clientName: ConnectionContract['dialectName'] + /** * A boolean to know if connection operates on read/write * replicas @@ -66,12 +76,18 @@ export class Connection extends EventEmitter implements ConnectionContract { ) { super() this.validateConfig() - this.dialectName = resolveClientNameWithAliases(this.config.client) + this.clientName = resolveClientNameWithAliases(this.config.client) + this.dialectName = this.clientName + this.hasReadWriteReplicas = !!( this.config.replicas && this.config.replicas.read && this.config.replicas.write ) + + if (!clientsNames.includes(this.clientName)) { + throw new errors.E_UNSUPPORTED_CLIENT([this.clientName]) + } } /** @@ -133,6 +149,17 @@ export class Connection extends EventEmitter implements ConnectionContract { */ private getWriteConfig(): Knex.Config { if (!this.config.replicas) { + /** + * Replacing string based libsql client with the + * actual implementation + */ + if (this.config.client === 'libsql') { + return { + ...this.config, + client: LibSQLClient, + } + } + return this.config } diff --git a/src/dialects/base_sqlite.ts b/src/dialects/base_sqlite.ts index 23aa1f94..d41f93ed 100644 --- a/src/dialects/base_sqlite.ts +++ b/src/dialects/base_sqlite.ts @@ -10,7 +10,7 @@ import type { DialectContract, QueryClientContract, SharedConfigNode } from '../types/database.js' export abstract class BaseSqliteDialect implements DialectContract { - abstract readonly name: 'sqlite3' | 'better-sqlite3' + abstract readonly name: 'sqlite3' | 'better-sqlite3' | 'libsql' readonly supportsAdvisoryLocks = false readonly supportsViews = true readonly supportsTypes = false diff --git a/src/dialects/index.ts b/src/dialects/index.ts index 87e24426..f628a4e3 100644 --- a/src/dialects/index.ts +++ b/src/dialects/index.ts @@ -10,14 +10,20 @@ import { PgDialect } from './pg.js' import { MysqlDialect } from './mysql.js' import { MssqlDialect } from './mssql.js' +import { LibSQLDialect } from './libsql.js' import { SqliteDialect } from './sqlite.js' import { OracleDialect } from './oracle.js' import { RedshiftDialect } from './red_shift.js' import { BetterSqliteDialect } from './better_sqlite.js' -import { DialectContract, QueryClientContract, SharedConfigNode } from '../types/database.js' +import { + DialectContract, + SharedConfigNode, + QueryClientContract, + ConnectionContract, +} from '../types/database.js' -export const dialects: { - [key: string]: { +export const clientsToDialectsMapping: { + [K in ConnectionContract['clientName']]: { new (client: QueryClientContract, config: SharedConfigNode): DialectContract } } = { @@ -28,5 +34,10 @@ export const dialects: { 'postgres': PgDialect, 'redshift': RedshiftDialect, 'sqlite3': SqliteDialect, + 'libsql': LibSQLDialect, 'better-sqlite3': BetterSqliteDialect, } + +export const clientsNames = Object.keys( + clientsToDialectsMapping +) as ConnectionContract['clientName'][] diff --git a/src/dialects/libsql.ts b/src/dialects/libsql.ts new file mode 100644 index 00000000..82949096 --- /dev/null +++ b/src/dialects/libsql.ts @@ -0,0 +1,15 @@ +/* + * @adonisjs/lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { DialectContract } from '../types/database.js' +import { BaseSqliteDialect } from './base_sqlite.js' + +export class LibSQLDialect extends BaseSqliteDialect implements DialectContract { + readonly name = 'libsql' +} diff --git a/src/errors.ts b/src/errors.ts index ba217231..2ccb60f9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -72,3 +72,12 @@ export const E_UNDEFINED_RELATIONSHIP = createError( ) export const E_RUNTIME_EXCEPTION = createError('%s', 'E_RUNTIME_EXCEPTION', 500) + +/** + * The client is not supported by Lucid + */ +export const E_UNSUPPORTED_CLIENT = createError<[string]>( + 'Unsupported client "%s"', + 'E_UNSUPPORTED_CLIENT', + 500 +) diff --git a/src/query_client/index.ts b/src/query_client/index.ts index 2cf5cd0f..f2a5f4b9 100644 --- a/src/query_client/index.ts +++ b/src/query_client/index.ts @@ -19,9 +19,9 @@ import { TransactionClientContract, } from '../types/database.js' -import { dialects } from '../dialects/index.js' -import { TransactionClient } from '../transaction_client/index.js' import { RawBuilder } from '../database/static_builder/raw.js' +import { clientsToDialectsMapping } from '../dialects/index.js' +import { TransactionClient } from '../transaction_client/index.js' import { RawQueryBuilder } from '../database/query_builder/raw.js' import { InsertQueryBuilder } from '../database/query_builder/insert.js' import { ReferenceBuilder } from '../database/static_builder/reference.js' @@ -72,7 +72,10 @@ export class QueryClient implements QueryClientContract { ) { this.debug = !!this.connection.config.debug this.connectionName = this.connection.name - this.dialect = new dialects[this.connection.dialectName](this, this.connection.config) + this.dialect = new clientsToDialectsMapping[this.connection.clientName]( + this, + this.connection.config + ) } /** diff --git a/src/query_runner/index.ts b/src/query_runner/index.ts index 8b96e550..92931e01 100644 --- a/src/query_runner/index.ts +++ b/src/query_runner/index.ts @@ -33,7 +33,7 @@ export class QueryRunner { * Is query dialect using sqlite database or not */ private isUsingSqlite() { - return this.client.dialect.name === 'sqlite3' + return ['sqlite3', 'better-sqlite3', 'libsql'].includes(this.client.dialect.name) } /** diff --git a/src/types/database.ts b/src/types/database.ts index b46b277a..5ec2cae9 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -55,6 +55,7 @@ export interface DialectContract { | 'postgres' | 'redshift' | 'sqlite3' + | 'libsql' | 'better-sqlite3' readonly dateTimeFormat: string @@ -371,6 +372,24 @@ export type SqliteConfig = SharedConfigNode & { replicas?: never } +/** + * The LibSQL specific config options are taken directly from the + * driver. https://github.com/mapbox/node-sqlite3/wiki/API#new-sqlite3databasefilename-mode-callback + * + * LibSQL dialect is a drop-in replacement for SQLite and hence the config + * options are same + */ +export type LibSQLConfig = SharedConfigNode & { + client: 'libsql' + connection: { + filename: string + flags?: string[] + debug?: boolean + mode?: any + } + replicas?: never +} + /** * The MYSQL specific config options are taken directly from the * driver. https://www.npmjs.com/package/mysql#connection-options @@ -543,6 +562,7 @@ export type MssqlConfig = SharedConfigNode & { */ export type ConnectionConfig = | SqliteConfig + | LibSQLConfig | MysqlConfig | PostgreConfig | OracleConfig @@ -638,6 +658,10 @@ export interface ConnectionContract extends EventEmitter { client?: Knex readClient?: Knex + /** + * @deprecated + * @see clientName + */ readonly dialectName: | 'mssql' | 'mysql' @@ -646,6 +670,19 @@ export interface ConnectionContract extends EventEmitter { | 'postgres' | 'redshift' | 'sqlite3' + | 'libsql' + | 'better-sqlite3' + + readonly clientName: + | 'mssql' + | 'mysql' + | 'mysql2' + | 'oracledb' + | 'postgres' + | 'redshift' + | 'sqlite3' + | 'libsql' + | 'better-sqlite3' /** * Property to find if explicit read/write is enabled diff --git a/test-helpers/index.ts b/test-helpers/index.ts index 53d4ce0a..d586507e 100644 --- a/test-helpers/index.ts +++ b/test-helpers/index.ts @@ -43,6 +43,9 @@ import { InsertQueryBuilder } from '../src/database/query_builder/insert.js' import { LucidRow, LucidModel, AdapterContract } from '../src/types/model.js' import { DatabaseQueryBuilder } from '../src/database/query_builder/database.js' +// @ts-expect-error +import LibSQLClient from '../src/clients/libsql.cjs' + dotenv.config() export const APP_ROOT = new URL('./tmp', import.meta.url) export const SQLITE_BASE_PATH = fileURLToPath(APP_ROOT) @@ -66,6 +69,15 @@ export function getConfig(): ConnectionConfig { useNullAsDefault: true, debug: !!process.env.DEBUG, } + case 'libsql': + return { + client: 'libsql', + connection: { + filename: `file:${join(SQLITE_BASE_PATH, 'libsql.db')}`, + }, + useNullAsDefault: true, + debug: !!process.env.DEBUG, + } case 'better_sqlite': return { client: 'better-sqlite3', @@ -138,11 +150,27 @@ export function getConfig(): ConnectionConfig { } } +/** + * Returns an instance of knex for testing + */ +export function getKnex(config: knex.Knex.Config): knex.Knex { + return knex.knex( + Object.assign( + {}, + { + ...config, + client: config.client === 'libsql' ? LibSQLClient : config.client, + }, + { debug: false } + ) + ) +} + /** * Does base setup by creating databases */ export async function setup(destroyDb: boolean = true) { - const db = knex.knex(Object.assign({}, getConfig(), { debug: false })) + const db = getKnex(Object.assign({}, getConfig(), { debug: false })) const hasUsersTable = await db.schema.hasTable('users') if (!hasUsersTable) { @@ -292,7 +320,7 @@ export async function setup(destroyDb: boolean = true) { * Does cleanup removes database */ export async function cleanup(customTables?: string[]) { - const db = knex.knex(Object.assign({}, getConfig(), { debug: false })) + const db = getKnex(Object.assign({}, getConfig(), { debug: false })) if (customTables) { for (let table of customTables) { @@ -324,7 +352,7 @@ export async function cleanup(customTables?: string[]) { * Reset database tables */ export async function resetTables() { - const db = knex.knex(Object.assign({}, getConfig(), { debug: false })) + const db = getKnex(Object.assign({}, getConfig(), { debug: false })) await db.table('users').truncate() await db.table('uuid_users').truncate() await db.table('follows').truncate() diff --git a/test-helpers/tmp/libsql.db b/test-helpers/tmp/libsql.db new file mode 100644 index 0000000000000000000000000000000000000000..417f413b72f570fed3916e1c9b5f26f16bb395ab GIT binary patch literal 102400 zcmeI)&u`o28NhK;vdt)tt^8)PpeTJP93f5=vvv>efM34+?V5aDzMXl-PXfy!+IaE|DcCGFGY!>Bzv`MEwcNyLrW&5_kEww zOHzDezR#0)-*!XC+HClBJG6@0SxwiqA6u4|`OTW9X;)>Lk;T-sul^yAQ?hiw^zM^A zA5|Hz`_Jd(1$zF4mVYt($!sq9KK(fNuiULupBaCcc|QHOsV}GIv!%@M^*`y?wHI@r zq)Kgn$281h@niW}DCq4Qp}kRag342;ZkH-G*YUz2{o9o{AFr;gty*gAQ8?-?lzvh`e|ev} z53aCel|8%el&xpBUwLZ#*YDiDTj=S@)(tzXJngl=e@orn^6gNNN};PtsM?{Uo>^6A z({9y5>&^AYk5?b9mDV0STz#^(^6;HPd*z~GE-dIf52GNr{6?!;Y6Xs;JiC+*FWGRY z*g9|T+c6{}RbMH+N_Gipe6?hUNf_#tO%+pmb)mgb@3ZLaDGhEmg4B9hwb7_!!%8OcfHev zQbnNZ3C^ZNiq;(}o)JB_cZhO4)P}k%{``>0jt%cmWZ4M&C1DKUK#j1HBGeDGGjVec-@^`9W4s)EkX z`4Mrw^b_RL8N+<_RefhMKFV5N=*#)B(ukkTr5oK_f2h9r#-~4j)-Zo?O>bXwy{hx^ zrnJD8FJ8rkFU>K%bYml^Vg=#$&OVwkVJrnfsC8(-L| zI=UB@(etb6_V>T+;6W5#-sP;OW&b=mm$TBE!im3})wIl8$Nh3v#&Mq=qsv(tLVgjE zpOnklcQP~PjT`z0ooj)QYBF>$1&*AFpVPC8dg}i0%xM)jfB!#@y-%AF zKmY**5I_I{1Q0*~0R#~E+5&S^Uurq+zRb;+8Tftk?`73~-%|d!Yr+)s|7A9@%#|CS zlGs0$nfd?B|Nr_)HDq^+XWaMCC6~2FH~)X;x%O}7|4SZ#<2(Of{!{zRw|D-(rk$Es zQy5R`{C`a|UhkVxI_dNOB^%PG$7ue)rcM9-|2_X->iqdJoc}+NxhKkKQjmmKzf-a7 znw?w*jaseot6;P9a5^=^wj8fLcp3nefnYexP^UQ8+ltaP417S2N`dpgQylCOV)l&g_H?=knfre;nxp+BB2HeGJTubJ z$%d|Vl#8nPMj@6QzDAV|M@Kf8mP-<&6beOS#{B7mu0@LRZ0k-2F-g2pN}60+PM)8V zlmN?Wq-IYW=CY}`jZPn^q!T5{Z01U)mncI=^atIU(d-VIJ z3w>3nVwF{YEmQl{GVuC=in?2)x_+iUv41<)=~C5{D1^*FM|RuTms+Mbnn>k7OXXry zof)ND*&aA5_D3{uXtU zA|2hHS2+6l|M7nmT$V?b+b<(aP8QiUHA5D4d%zi4W@S-R1?FYR%W_^8b=SbOEazmo zAj?HrF3FkjQ|1&AbeW&!aQvY3?Czc0cfNb`_8T{szi-{XcYnEf|L(mX>C@T=`@g$Va~&_- z { group.setup(async () => { await setup() @@ -122,28 +122,6 @@ test.group('Connection | setup', (group) => { await connection.disconnect() }).waitForDone() - - test('raise error when unable to make connection', ({ assert }, done) => { - assert.plan(2) - - const connection = new Connection( - 'primary', - Object.assign({}, getConfig(), { client: null }), - logger - ) - - connection.on('error', ({ message }) => { - try { - assert.equal(message, "knex: Required configuration option 'client' is missing.") - done() - } catch (error) { - done(error) - } - }) - - const fn = () => connection.connect() - assert.throws(fn, /knex: Required configuration option/) - }).waitForDone() }) if (process.env.DB === 'mysql') { diff --git a/test/connection/connection_manager.spec.ts b/test/connection/connection_manager.spec.ts index b26efd84..403e1ec6 100644 --- a/test/connection/connection_manager.spec.ts +++ b/test/connection/connection_manager.spec.ts @@ -12,8 +12,8 @@ import { test } from '@japa/runner' import { Connection } from '../../src/connection/index.js' import { ConnectionManager } from '../../src/connection/manager.js' import { - getConfig, setup, + getConfig, cleanup, mapToObj, logger, @@ -117,29 +117,6 @@ test.group('ConnectionManager', (group) => { assert.isFalse(manager.has('primary')) }) - test('proxy error event', async ({ assert }, done) => { - assert.plan(3) - - const emitter = createEmitter() - const manager = new ConnectionManager(logger, emitter) - manager.add('primary', Object.assign({}, getConfig(), { client: null })) - - emitter.on('db:connection:error', async ([{ message }, connection]) => { - try { - assert.equal(message, "knex: Required configuration option 'client' is missing.") - assert.instanceOf(connection, Connection) - await manager.closeAll() - done() - } catch (error) { - await manager.closeAll() - done(error) - } - }) - - const fn = () => manager.connect('primary') - assert.throws(fn, /knex: Required configuration option/) - }).waitForDone() - test('patching the connection config must close old and create a new connection', async ({ assert, }, done) => { diff --git a/test/database/db_connection_count_check.spec.ts b/test/database/db_connection_count_check.spec.ts index 32e30939..2d0cc4c9 100644 --- a/test/database/db_connection_count_check.spec.ts +++ b/test/database/db_connection_count_check.spec.ts @@ -29,8 +29,9 @@ test.group('Db connection count check', (group) => { } const db = new Database(config, logger, createEmitter()) + const client = db.connection() - const healthCheck = new DbConnectionCountCheck(db.connection()).compute(async () => { + const healthCheck = new DbConnectionCountCheck(client).compute(async () => { return 20 }) @@ -39,7 +40,7 @@ test.group('Db connection count check', (group) => { message: 'There are 20 active connections, which is above the threshold of 15 connections', status: 'error', meta: { - connection: { name: 'primary', dialect: config.connections.primary.client }, + connection: { name: 'primary', dialect: client.dialect.name }, connectionsCount: { active: 20, failureThreshold: 15, @@ -58,8 +59,9 @@ test.group('Db connection count check', (group) => { } const db = new Database(config, logger, createEmitter()) + const client = db.connection() - const healthCheck = new DbConnectionCountCheck(db.connection()).compute(async () => { + const healthCheck = new DbConnectionCountCheck(client).compute(async () => { return 12 }) @@ -68,7 +70,7 @@ test.group('Db connection count check', (group) => { message: 'There are 12 active connections, which is above the threshold of 10 connections', status: 'warning', meta: { - connection: { name: 'primary', dialect: config.connections.primary.client }, + connection: { name: 'primary', dialect: client.dialect.name }, connectionsCount: { active: 12, failureThreshold: 15, @@ -87,8 +89,9 @@ test.group('Db connection count check', (group) => { } const db = new Database(config, logger, createEmitter()) + const client = db.connection() - const healthCheck = new DbConnectionCountCheck(db.connection()).compute(async () => { + const healthCheck = new DbConnectionCountCheck(client).compute(async () => { return null }) @@ -97,7 +100,7 @@ test.group('Db connection count check', (group) => { message: `Check skipped. Unable to get active connections for ${config.connections.primary.client} dialect`, status: 'ok', meta: { - connection: { name: 'primary', dialect: config.connections.primary.client }, + connection: { name: 'primary', dialect: client.dialect.name }, }, }) @@ -111,8 +114,9 @@ test.group('Db connection count check', (group) => { } const db = new Database(config, logger, createEmitter()) + const client = db.connection() - const healthCheck = new DbConnectionCountCheck(db.connection()) + const healthCheck = new DbConnectionCountCheck(client) const result = await healthCheck.run() const activeConnections = result.meta?.connectionsCount.active @@ -121,7 +125,7 @@ test.group('Db connection count check', (group) => { message: `There are ${activeConnections} active connections, which is under the defined thresholds`, status: 'ok', meta: { - connection: { name: 'primary', dialect: db.connection().dialect.name }, + connection: { name: 'primary', dialect: client.dialect.name }, connectionsCount: { active: activeConnections, failureThreshold: 15, @@ -140,8 +144,9 @@ test.group('Db connection count check', (group) => { } const db = new Database(config, logger, createEmitter()) + const client = db.connection() - const healthCheck = new DbConnectionCountCheck(db.connection()) + const healthCheck = new DbConnectionCountCheck(client) const result = await healthCheck.run() const activeConnections = result.meta?.connectionsCount.active @@ -150,7 +155,7 @@ test.group('Db connection count check', (group) => { message: `There are ${activeConnections} active connections, which is under the defined thresholds`, status: 'ok', meta: { - connection: { name: 'primary', dialect: db.connection().dialect.name }, + connection: { name: 'primary', dialect: client.dialect.name }, connectionsCount: { active: activeConnections, failureThreshold: 15, diff --git a/test/database/drop_tables.spec.ts b/test/database/drop_tables.spec.ts index d863e354..1e1194e8 100644 --- a/test/database/drop_tables.spec.ts +++ b/test/database/drop_tables.spec.ts @@ -134,7 +134,7 @@ test.group('Query client | drop tables', (group) => { await connection.disconnect() }) - if (['better-sqlite', 'sqlite'].includes(process.env.DB!)) { + if (['better-sqlite', 'sqlite', 'libsql'].includes(process.env.DB!)) { test('drop tables when PRAGMA foreign_keys is enabled', async ({ assert }) => { const connection = new Connection('primary', getConfig(), logger) connection.connect() diff --git a/test/database/query_builder.spec.ts b/test/database/query_builder.spec.ts index b81e807e..07e2274e 100644 --- a/test/database/query_builder.spec.ts +++ b/test/database/query_builder.spec.ts @@ -27,7 +27,7 @@ import { } from '../../test-helpers/index.js' import { QueryClient } from '../../src/query_client/index.js' -if (process.env.DB !== 'sqlite') { +if (!['better-sqlite', 'sqlite', 'libsql'].includes(process.env.DB!)) { test.group('Query Builder | client', (group) => { group.setup(async () => { await setup() @@ -11600,7 +11600,7 @@ test.group('Query Builder | withRecursive', (group) => { }) }) -if (['pg', 'sqlite', 'better_sqlite'].includes(process.env.DB!)) { +if (['pg', 'sqlite', 'better_sqlite', 'libsql'].includes(process.env.DB!)) { test.group('Query Builder | withMaterialized', (group) => { group.setup(async () => { await setup() diff --git a/test/database/query_client.spec.ts b/test/database/query_client.spec.ts index 75ae17cd..4aea481f 100644 --- a/test/database/query_client.spec.ts +++ b/test/database/query_client.spec.ts @@ -206,12 +206,11 @@ test.group('Query client | dual mode', (group) => { connection.connect() const client = new QueryClient('dual', connection, createEmitter()) - const command = - process.env.DB === 'sqlite' || process.env.DB === 'better_sqlite' - ? 'DELETE FROM users;' - : process.env.DB === 'mssql' - ? 'TRUNCATE table users;' - : 'TRUNCATE users;' + const command = ['better_sqlite', 'sqlite', 'libsql'].includes(process.env.DB!) + ? 'DELETE FROM users;' + : process.env.DB === 'mssql' + ? 'TRUNCATE table users;' + : 'TRUNCATE users;' await client.insertQuery().table('users').insert({ username: 'virk' }) await client.rawQuery(command).exec() @@ -353,12 +352,11 @@ test.group('Query client | write mode', (group) => { connection.connect() const client = new QueryClient('write', connection, createEmitter()) - const command = - process.env.DB === 'sqlite' || process.env.DB === 'better_sqlite' - ? 'DELETE FROM users;' - : process.env.DB === 'mssql' - ? 'TRUNCATE table users;' - : 'TRUNCATE users;' + const command = ['better_sqlite', 'sqlite', 'libsql'].includes(process.env.DB!) + ? 'DELETE FROM users;' + : process.env.DB === 'mssql' + ? 'TRUNCATE table users;' + : 'TRUNCATE users;' await client.insertQuery().table('users').insert({ username: 'virk' }) await client.rawQuery(command).exec() @@ -388,7 +386,7 @@ test.group('Query client | write mode', (group) => { }) }) -if (!['sqlite', 'mssql', 'better_sqlite'].includes(process.env.DB as string)) { +if (!['sqlite', 'mssql', 'better_sqlite', 'libsql'].includes(process.env.DB!)) { test.group('Query client | advisory locks', (group) => { group.setup(async () => { await setup() @@ -419,11 +417,6 @@ if (!['sqlite', 'mssql', 'better_sqlite'].includes(process.env.DB as string)) { connection.connect() const client = new QueryClient('dual', connection, createEmitter()) - if (client.dialect.name === 'sqlite3') { - await connection.disconnect() - return - } - await client.dialect.getAdvisoryLock(1) const released = await client.dialect.releaseAdvisoryLock(1) assert.isTrue(released) diff --git a/test/database/views_types.spec.ts b/test/database/views_types.spec.ts index f04c7f50..16827e2c 100644 --- a/test/database/views_types.spec.ts +++ b/test/database/views_types.spec.ts @@ -23,7 +23,7 @@ test.group('Query client | Views, types and domains', (group) => { await cleanup() }) - if (['sqlite', 'mysql', 'pg'].includes(process.env.DB!)) { + if (['sqlite', 'mysql', 'pg', 'libsql'].includes(process.env.DB!)) { test('Get all views', async ({ assert }) => { const connection = new Connection('primary', getConfig(), logger) connection.connect() diff --git a/test/migrations/schema.spec.ts b/test/migrations/schema.spec.ts index e4218ee7..cd722d49 100644 --- a/test/migrations/schema.spec.ts +++ b/test/migrations/schema.spec.ts @@ -226,12 +226,9 @@ test.group('Schema', (group) => { } async down() { - if (this.db.dialect.name !== 'sqlite3') { - this.schema.table('schema_accounts', (table) => { - table.dropForeign(['user_id']) - }) - } - + this.schema.table('schema_accounts', (table) => { + table.dropForeign(['user_id']) + }) this.schema.dropTable('schema_users') this.schema.dropTable('schema_accounts') } From 7fedbfe35031be9b259778294e53145c97466e5a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 18 Jun 2024 14:47:48 +0530 Subject: [PATCH 54/73] refactor: update usages of dialectName with clientName --- src/connection/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connection/index.ts b/src/connection/index.ts index f6d731c1..b999167d 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -45,12 +45,12 @@ export class Connection extends EventEmitter implements ConnectionContract { * @deprecated * @see clientName */ - dialectName: ConnectionContract['dialectName'] + dialectName: ConnectionContract['clientName'] /** * Connection client name. */ - clientName: ConnectionContract['dialectName'] + clientName: ConnectionContract['clientName'] /** * A boolean to know if connection operates on read/write From 2d9697b550f3546f98d59bf43b18424e831786bb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 18 Jun 2024 21:27:13 +0530 Subject: [PATCH 55/73] test: fix breaking tests --- test/database/db_connection_count_check.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/database/db_connection_count_check.spec.ts b/test/database/db_connection_count_check.spec.ts index 2d0cc4c9..e8865643 100644 --- a/test/database/db_connection_count_check.spec.ts +++ b/test/database/db_connection_count_check.spec.ts @@ -97,7 +97,7 @@ test.group('Db connection count check', (group) => { const result = await healthCheck.run() assert.containsSubset(result, { - message: `Check skipped. Unable to get active connections for ${config.connections.primary.client} dialect`, + message: `Check skipped. Unable to get active connections for ${client.dialect.name} dialect`, status: 'ok', meta: { connection: { name: 'primary', dialect: client.dialect.name }, From 8a4df1cd9fe96b8df31680dea63a7c83707204eb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 18 Jun 2024 21:31:25 +0530 Subject: [PATCH 56/73] test: another attempt at fixing broken tests --- test/database/drop_tables.spec.ts | 2 +- test/database/query_builder.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/database/drop_tables.spec.ts b/test/database/drop_tables.spec.ts index 1e1194e8..bccb81aa 100644 --- a/test/database/drop_tables.spec.ts +++ b/test/database/drop_tables.spec.ts @@ -134,7 +134,7 @@ test.group('Query client | drop tables', (group) => { await connection.disconnect() }) - if (['better-sqlite', 'sqlite', 'libsql'].includes(process.env.DB!)) { + if (['better_sqlite', 'sqlite', 'libsql'].includes(process.env.DB!)) { test('drop tables when PRAGMA foreign_keys is enabled', async ({ assert }) => { const connection = new Connection('primary', getConfig(), logger) connection.connect() diff --git a/test/database/query_builder.spec.ts b/test/database/query_builder.spec.ts index 07e2274e..4d6d5d40 100644 --- a/test/database/query_builder.spec.ts +++ b/test/database/query_builder.spec.ts @@ -27,7 +27,7 @@ import { } from '../../test-helpers/index.js' import { QueryClient } from '../../src/query_client/index.js' -if (!['better-sqlite', 'sqlite', 'libsql'].includes(process.env.DB!)) { +if (!['better_sqlite', 'sqlite', 'libsql'].includes(process.env.DB!)) { test.group('Query Builder | client', (group) => { group.setup(async () => { await setup() From 9fc57d6bb7e79df3537586f8f564f818ed1a6037 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 18 Jun 2024 21:36:20 +0530 Subject: [PATCH 57/73] chore(release): 21.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b401a622..f9ca3861 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/lucid", "description": "SQL ORM built on top of Active Record pattern", - "version": "20.6.0", + "version": "21.0.0", "engines": { "node": ">=18.16.0" }, From 1afc648fa8d313763c1bf217017165c4ca676e31 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 19 Jun 2024 07:34:19 +0530 Subject: [PATCH 58/73] fix: broken import of libsql client --- src/clients/libsql.cjs | 15 ++++++++------- src/connection/index.ts | 5 ++--- test-helpers/index.ts | 6 ++---- test-helpers/tmp/libsql.db | Bin 102400 -> 102400 bytes tsconfig.json | 3 ++- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/clients/libsql.cjs b/src/clients/libsql.cjs index 19d6dca6..e20bb475 100644 --- a/src/clients/libsql.cjs +++ b/src/clients/libsql.cjs @@ -1,14 +1,15 @@ const Sqlite3Client = require('knex/lib/dialects/sqlite3') -class LibSQLClient extends Sqlite3Client { +module.exports = class LibSQLClient extends Sqlite3Client { _driver() { return require('@libsql/sqlite3') } -} -Object.assign(LibSQLClient.prototype, { - dialect: 'libsql', - driverName: 'libsql', -}) + get dialect() { + return 'libsql' + } -module.exports = LibSQLClient + get driverName() { + return 'libsql' + } +} diff --git a/src/connection/index.ts b/src/connection/index.ts index b999167d..37eee0f1 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -16,9 +16,8 @@ import type { Logger } from '@adonisjs/core/logger' import { resolveClientNameWithAliases } from 'knex/lib/util/helpers.js' import * as errors from '../errors.js' -import { clientsNames } from '../dialects/index.js' -// @ts-expect-error import LibSQLClient from '../clients/libsql.cjs' +import { clientsNames } from '../dialects/index.js' import { Logger as ConnectionLogger } from './logger.js' import type { ConnectionConfig, ConnectionContract } from '../types/database.js' @@ -156,7 +155,7 @@ export class Connection extends EventEmitter implements ConnectionContract { if (this.config.client === 'libsql') { return { ...this.config, - client: LibSQLClient, + client: LibSQLClient as any, } } diff --git a/test-helpers/index.ts b/test-helpers/index.ts index d586507e..0c3d7981 100644 --- a/test-helpers/index.ts +++ b/test-helpers/index.ts @@ -33,6 +33,7 @@ import { import { BaseSchema } from '../src/schema/main.js' import { Database } from '../src/database/main.js' +import LibSQLClient from '../src/clients/libsql.cjs' import { Adapter } from '../src/orm/adapter/index.js' import { BaseModel } from '../src/orm/base_model/index.js' import { QueryClient } from '../src/query_client/index.js' @@ -43,9 +44,6 @@ import { InsertQueryBuilder } from '../src/database/query_builder/insert.js' import { LucidRow, LucidModel, AdapterContract } from '../src/types/model.js' import { DatabaseQueryBuilder } from '../src/database/query_builder/database.js' -// @ts-expect-error -import LibSQLClient from '../src/clients/libsql.cjs' - dotenv.config() export const APP_ROOT = new URL('./tmp', import.meta.url) export const SQLITE_BASE_PATH = fileURLToPath(APP_ROOT) @@ -159,7 +157,7 @@ export function getKnex(config: knex.Knex.Config): knex.Knex { {}, { ...config, - client: config.client === 'libsql' ? LibSQLClient : config.client, + client: config.client === 'libsql' ? (LibSQLClient as any) : config.client, }, { debug: false } ) diff --git a/test-helpers/tmp/libsql.db b/test-helpers/tmp/libsql.db index 417f413b72f570fed3916e1c9b5f26f16bb395ab..dc35f57bcbfe13186006d41495f4def5083415c1 100644 GIT binary patch delta 99 zcmZozz}B#UZGx20%{dGV43a?12E^hFjEYhdb&Qy9&e@o-TAuk9|EWN2k7qPTaJT=DXJj-00DIjWmS?`he`hnF!a05(US?)R t&f@IMoSgX5<`??gU+6QQ)918MFtD^TG`2FGzCWJP9KqfGKc11%003$I9E<<} diff --git a/tsconfig.json b/tsconfig.json index ad0cc44a..e6fe66f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { "rootDir": "./", - "outDir": "./build" + "outDir": "./build", + "allowJs": true } } From 62e3c2fae6fb94ed081ea82bc4cd8668b0e5a6a3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 19 Jun 2024 07:35:04 +0530 Subject: [PATCH 59/73] chore(release): 21.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f9ca3861..5155b4a9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/lucid", "description": "SQL ORM built on top of Active Record pattern", - "version": "21.0.0", + "version": "21.0.1", "engines": { "node": ">=18.16.0" }, From 596c15341099a27ec7a5675f6a314b850b0d5704 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 19 Jun 2024 07:57:25 +0530 Subject: [PATCH 60/73] refactor: configure command to display dialect name in prompts --- configure.ts | 7 ++++++- package.json | 6 +++--- test/configure.spec.ts | 41 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/configure.ts b/configure.ts index 2e4bef33..ac740d8a 100644 --- a/configure.ts +++ b/configure.ts @@ -25,7 +25,12 @@ export async function configure(command: Configure) { if (dialect === undefined) { dialect = await command.prompt.choice( 'Select the database you want to use', - Object.keys(DIALECTS), + Object.keys(DIALECTS).map((dialectKey) => { + return { + name: dialectKey, + message: DIALECTS[dialectKey as keyof typeof DIALECTS].name, + } + }), { validate(value) { return !!value diff --git a/package.json b/package.json index 5155b4a9..dd69f8ac 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@swc/core": "^1.6.1", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.4", + "@types/node": "^20.14.5", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.15", "@vinejs/vine": "^2.1.0", @@ -90,7 +90,7 @@ "husky": "^9.0.11", "luxon": "^3.4.4", "mysql2": "^3.10.1", - "np": "^10.0.5", + "np": "^10.0.6", "pg": "^8.12.0", "prettier": "^3.3.2", "reflect-metadata": "^0.2.2", @@ -100,7 +100,7 @@ "typescript": "^5.4.5" }, "dependencies": { - "@adonisjs/presets": "^2.4.1", + "@adonisjs/presets": "^2.5.1", "@faker-js/faker": "^8.4.1", "@poppinss/hooks": "^7.2.3", "@poppinss/macroable": "^1.0.2", diff --git a/test/configure.spec.ts b/test/configure.spec.ts index 6e87e914..878a2630 100644 --- a/test/configure.spec.ts +++ b/test/configure.spec.ts @@ -133,7 +133,42 @@ test.group('Configure', (group) => { const ace = await app.container.make('ace') ace.prompt.trap('Select the database you want to use').chooseOption(0) - ace.prompt.trap('Do you want to install additional packages required by "@adonisjs/lucid"?') + ace.prompt + .trap('Do you want to install additional packages required by "@adonisjs/lucid"?') + .reject() + + const command = await ace.create(Configure, ['../../index.js']) + await command.exec() + + await assert.dirExists('tmp') + }) + + test('create tmp directory for libsql dialect', async ({ fs, assert }) => { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await app.container.make('ace') + ace.prompt.trap('Select the database you want to use').chooseOption(1) + ace.prompt + .trap('Do you want to install additional packages required by "@adonisjs/lucid"?') + .reject() const command = await ace.create(Configure, ['../../index.js']) await command.exec() @@ -165,7 +200,9 @@ test.group('Configure', (group) => { const ace = await app.container.make('ace') ace.prompt.trap('Select the database you want to use').chooseOption(0) - ace.prompt.trap('Do you want to install additional packages required by "@adonisjs/lucid"?') + ace.prompt + .trap('Do you want to install additional packages required by "@adonisjs/lucid"?') + .reject() const command = await ace.create(Configure, ['../../index.js']) await command.exec() From 9fe4c2ec4234570456653bd028ecc751f3f4a0f7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 19 Jun 2024 08:09:43 +0530 Subject: [PATCH 61/73] chore(release): 21.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dd69f8ac..446997a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/lucid", "description": "SQL ORM built on top of Active Record pattern", - "version": "21.0.1", + "version": "21.1.0", "engines": { "node": ">=18.16.0" }, From 2d25ef00e53ea179e45ce0c68a00cd0f1d5112b5 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 19 Jun 2024 09:39:53 +0530 Subject: [PATCH 62/73] chore: update dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 446997a7..74316cb1 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "typescript": "^5.4.5" }, "dependencies": { - "@adonisjs/presets": "^2.5.1", + "@adonisjs/presets": "^2.6.1", "@faker-js/faker": "^8.4.1", "@poppinss/hooks": "^7.2.3", "@poppinss/macroable": "^1.0.2", From 2aee01a0f01768fda0bb77ac951f52de137ec5ad Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jul 2024 10:12:14 +0530 Subject: [PATCH 63/73] chore: update dependencies --- package.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 74316cb1..87456d60 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ }, "devDependencies": { "@adonisjs/assembler": "^7.7.0", - "@adonisjs/core": "^6.11.0", + "@adonisjs/core": "^6.12.1", "@adonisjs/eslint-config": "^1.3.0", "@adonisjs/prettier-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", @@ -70,16 +70,16 @@ "@japa/file-system": "^2.3.0", "@japa/runner": "^3.1.4", "@libsql/sqlite3": "^0.3.1", - "@swc/core": "^1.6.1", + "@swc/core": "^1.7.1", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.5", + "@types/node": "^20.14.12", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.15", "@vinejs/vine": "^2.1.0", - "better-sqlite3": "^11.0.0", + "better-sqlite3": "^11.1.2", "c8": "^10.1.2", - "chance": "^1.1.11", + "chance": "^1.1.12", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0", @@ -87,17 +87,17 @@ "eslint": "^8.57.0", "fs-extra": "^11.2.0", "github-label-sync": "^2.3.1", - "husky": "^9.0.11", + "husky": "^9.1.1", "luxon": "^3.4.4", - "mysql2": "^3.10.1", - "np": "^10.0.6", + "mysql2": "^3.10.3", + "np": "^10.0.7", "pg": "^8.12.0", - "prettier": "^3.3.2", + "prettier": "^3.3.3", "reflect-metadata": "^0.2.2", "sqlite3": "^5.1.7", - "tedious": "^18.2.0", + "tedious": "^18.3.0", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.5.4" }, "dependencies": { "@adonisjs/presets": "^2.6.1", @@ -111,7 +111,7 @@ "knex": "^3.1.0", "knex-dynamic-connection": "^3.2.0", "pretty-hrtime": "^1.0.3", - "qs": "^6.12.1", + "qs": "^6.12.3", "slash": "^5.1.0", "tarn": "^3.0.2" }, From aa0a573d3cd710d3f4c3f0fd52ed7536a81a0f1a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jul 2024 10:13:05 +0530 Subject: [PATCH 64/73] fix: cleanup of resources when using replicas Closes: #1045 --- src/connection/index.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/connection/index.ts b/src/connection/index.ts index 37eee0f1..c63f24ac 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -105,16 +105,6 @@ export class Connection extends EventEmitter implements ConnectionContract { } } - /** - * Cleanup references - */ - private cleanup(): void { - this.client = undefined - this.readClient = undefined - this.readReplicas = [] - this.roundRobinCounter = 0 - } - /** * Does cleanup by removing knex reference and removing all listeners. * For the same of simplicity, we get rid of both read and write @@ -127,7 +117,7 @@ export class Connection extends EventEmitter implements ConnectionContract { */ this.pool!.on('poolDestroySuccess', () => { this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource') - this.cleanup() + this.client = undefined this.emit('disconnect', this) this.removeAllListeners() }) @@ -135,7 +125,9 @@ export class Connection extends EventEmitter implements ConnectionContract { if (this.readPool !== this.pool) { this.readPool!.on('poolDestroySuccess', () => { this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource') - this.cleanup() + this.roundRobinCounter = 0 + this.readClient = undefined + this.readReplicas = [] this.emit('disconnect', this) this.removeAllListeners() }) From a9eac4b3d037bea7e9b5e7ba934171d0bf3346b6 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jul 2024 10:21:00 +0530 Subject: [PATCH 65/73] fix: connection resource cleanup logic --- src/connection/index.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/connection/index.ts b/src/connection/index.ts index c63f24ac..0ff32068 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -105,6 +105,26 @@ export class Connection extends EventEmitter implements ConnectionContract { } } + /** + * Cleans up reference for the write client and also the + * read client when not using replicas + */ + private cleanupWriteClient() { + if (this.client === this.readClient) { + this.cleanupReadClient() + } + this.client = undefined + } + + /** + * Cleans up reference for the read client + */ + private cleanupReadClient() { + this.roundRobinCounter = 0 + this.readClient = undefined + this.readReplicas = [] + } + /** * Does cleanup by removing knex reference and removing all listeners. * For the same of simplicity, we get rid of both read and write @@ -117,7 +137,7 @@ export class Connection extends EventEmitter implements ConnectionContract { */ this.pool!.on('poolDestroySuccess', () => { this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource') - this.client = undefined + this.cleanupWriteClient() this.emit('disconnect', this) this.removeAllListeners() }) @@ -125,9 +145,7 @@ export class Connection extends EventEmitter implements ConnectionContract { if (this.readPool !== this.pool) { this.readPool!.on('poolDestroySuccess', () => { this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource') - this.roundRobinCounter = 0 - this.readClient = undefined - this.readReplicas = [] + this.cleanupReadClient() this.emit('disconnect', this) this.removeAllListeners() }) From 2e002b7c36707a83e50aba1a072df9ec4fb9888f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jul 2024 10:25:05 +0530 Subject: [PATCH 66/73] test: add test for connection resource cleanup with replicas --- test/connection/connection.spec.ts | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/connection/connection.spec.ts b/test/connection/connection.spec.ts index aaf55c41..e49d4162 100644 --- a/test/connection/connection.spec.ts +++ b/test/connection/connection.spec.ts @@ -122,6 +122,38 @@ test.group('Connection | setup', (group) => { await connection.disconnect() }).waitForDone() + + test('cleanup read/write clients when connection is closed', async ({ assert }) => { + let disconnectEmitsCount = 0 + + const config = getConfig() + config.replicas! = { + write: { + connection: { + host: '10.0.0.1', + }, + }, + read: { + connection: [ + { + host: '10.0.0.1', + }, + ], + }, + } + + const connection = new Connection('primary', config, logger) + connection.connect() + connection.on('disconnect', () => { + disconnectEmitsCount++ + }) + + await connection.disconnect() + + assert.equal(disconnectEmitsCount, 2) + assert.isUndefined(connection.client) + assert.isUndefined(connection.readClient) + }).skip(['sqlite', 'better_sqlite', 'libsql'].includes(process.env.DB!)) }) if (process.env.DB === 'mysql') { From 14f345a01cfd4667e6f1c61060203a955733f9e1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jul 2024 10:52:02 +0530 Subject: [PATCH 67/73] test: fix failing tests --- test/connection/connection.spec.ts | 5 +- test/database/db_check.spec.ts | 17 +++--- .../db_connection_count_check.spec.ts | 52 ++++++++++++------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/test/connection/connection.spec.ts b/test/connection/connection.spec.ts index e49d4162..d1922ce0 100644 --- a/test/connection/connection.spec.ts +++ b/test/connection/connection.spec.ts @@ -144,7 +144,10 @@ test.group('Connection | setup', (group) => { const connection = new Connection('primary', config, logger) connection.connect() - connection.on('disconnect', () => { + connection.readPool?.on('poolDestroySuccess', () => { + disconnectEmitsCount++ + }) + connection.pool?.on('poolDestroySuccess', () => { disconnectEmitsCount++ }) diff --git a/test/database/db_check.spec.ts b/test/database/db_check.spec.ts index e1369b44..5266e372 100644 --- a/test/database/db_check.spec.ts +++ b/test/database/db_check.spec.ts @@ -22,26 +22,27 @@ test.group('Db connection check', (group) => { await cleanup() }) - test('perform health check for a connection', async ({ assert }) => { + test('perform health check for a connection', async ({ assert, cleanup: teardown }) => { const config = { connection: 'primary', connections: { primary: getConfig() }, } const db = new Database(config, logger, createEmitter()) + teardown(async () => { + await db.manager.closeAll() + }) const healthCheck = new DbCheck(db.connection()) const result = await healthCheck.run() assert.containsSubset(result, { message: 'Successfully connected to the database server', status: 'ok', - meta: { connection: { name: 'primary', dialect: config.connections.primary.client } }, + meta: { connection: { name: 'primary', dialect: db.connection().dialect.name } }, }) - - await db.manager.closeAll() }) - test('report error when unable to connect', async ({ assert }) => { + test('report error when unable to connect', async ({ assert, cleanup: teardown }) => { const config = { connection: 'primary', connections: { @@ -56,16 +57,18 @@ test.group('Db connection check', (group) => { } const db = new Database(config, logger, createEmitter()) + teardown(async () => { + await db.manager.closeAll() + }) const healthCheck = new DbCheck(db.connection()) const result = await healthCheck.run() + assert.containsSubset(result, { message: 'Connection failed', status: 'error', meta: { connection: { name: 'primary', dialect: 'mysql' } }, }) assert.equal(result.meta?.error.code, 'ECONNREFUSED') - - await db.manager.closeAll() }) }) diff --git a/test/database/db_connection_count_check.spec.ts b/test/database/db_connection_count_check.spec.ts index e8865643..0bfa8efb 100644 --- a/test/database/db_connection_count_check.spec.ts +++ b/test/database/db_connection_count_check.spec.ts @@ -22,15 +22,21 @@ test.group('Db connection count check', (group) => { await cleanup() }) - test('return error when failure threshold has been crossed', async ({ assert }) => { + test('return error when failure threshold has been crossed', async ({ + assert, + cleanup: teardown, + }) => { const config = { connection: 'primary', connections: { primary: getConfig() }, } const db = new Database(config, logger, createEmitter()) - const client = db.connection() + teardown(async () => { + await db.manager.closeAll() + }) + const client = db.connection() const healthCheck = new DbConnectionCountCheck(client).compute(async () => { return 20 }) @@ -48,19 +54,23 @@ test.group('Db connection count check', (group) => { }, }, }) - - await db.manager.closeAll() }) - test('return warning when warning threshold has been crossed', async ({ assert }) => { + test('return warning when warning threshold has been crossed', async ({ + assert, + cleanup: teardown, + }) => { const config = { connection: 'primary', connections: { primary: getConfig() }, } const db = new Database(config, logger, createEmitter()) - const client = db.connection() + teardown(async () => { + await db.manager.closeAll() + }) + const client = db.connection() const healthCheck = new DbConnectionCountCheck(client).compute(async () => { return 12 }) @@ -78,17 +88,22 @@ test.group('Db connection count check', (group) => { }, }, }) - - await db.manager.closeAll() }) - test('return success when unable to compute connections count', async ({ assert }) => { + test('return success when unable to compute connections count', async ({ + assert, + cleanup: teardown, + }) => { const config = { connection: 'primary', connections: { primary: getConfig() }, } const db = new Database(config, logger, createEmitter()) + teardown(async () => { + await db.manager.closeAll() + }) + const client = db.connection() const healthCheck = new DbConnectionCountCheck(client).compute(async () => { @@ -103,17 +118,19 @@ test.group('Db connection count check', (group) => { connection: { name: 'primary', dialect: client.dialect.name }, }, }) - - await db.manager.closeAll() }) - test('get PostgreSQL connections count', async ({ assert }) => { + test('get PostgreSQL connections count', async ({ assert, cleanup: teardown }) => { const config = { connection: 'primary', connections: { primary: getConfig() }, } const db = new Database(config, logger, createEmitter()) + teardown(async () => { + await db.manager.closeAll() + }) + const client = db.connection() const healthCheck = new DbConnectionCountCheck(client) @@ -133,19 +150,20 @@ test.group('Db connection count check', (group) => { }, }, }) - - await db.manager.closeAll() }).skip(process.env.DB !== 'pg', 'Only for PostgreSQL') - test('get MySQL connections count', async ({ assert }) => { + test('get MySQL connections count', async ({ assert, cleanup: teardown }) => { const config = { connection: 'primary', connections: { primary: getConfig() }, } const db = new Database(config, logger, createEmitter()) - const client = db.connection() + teardown(async () => { + await db.manager.closeAll() + }) + const client = db.connection() const healthCheck = new DbConnectionCountCheck(client) const result = await healthCheck.run() @@ -163,7 +181,5 @@ test.group('Db connection count check', (group) => { }, }, }) - - await db.manager.closeAll() }).skip(!['mysql', 'mysql_legacy'].includes(process.env.DB!), 'Only for MySQL') }) From 694620357d7dda90a6bf85f575526793e4fc17e1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 25 Jul 2024 10:56:35 +0530 Subject: [PATCH 68/73] chore(release): 21.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87456d60..273d33c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/lucid", "description": "SQL ORM built on top of Active Record pattern", - "version": "21.1.0", + "version": "21.1.1", "engines": { "node": ">=18.16.0" }, From c131bc7d3ea1470e74118c9bc097ded85f49b64f Mon Sep 17 00:00:00 2001 From: Andrej Adamcik Date: Thu, 8 Aug 2024 07:00:23 +0200 Subject: [PATCH 69/73] fix: apply constraints in whereIn condition (#1037) --- src/database/query_builder/chainable.ts | 11 +++++- src/orm/relations/base/query_builder.ts | 8 ++++ test/orm/model_query_builder.spec.ts | 50 ++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/database/query_builder/chainable.ts b/src/database/query_builder/chainable.ts index 7839ff3e..d7ec061b 100644 --- a/src/database/query_builder/chainable.ts +++ b/src/database/query_builder/chainable.ts @@ -231,8 +231,7 @@ export abstract class Chainable extends Macroable implements ChainableContract { */ protected transformValue(value: any) { if (value instanceof Chainable) { - value.applyWhere() - return value.knexQuery + return value.toKnex() } if (value instanceof ReferenceBuilder) { @@ -2074,4 +2073,12 @@ export abstract class Chainable extends Macroable implements ChainableContract { return this } + + /** + * Applies statements and returns knex query + */ + toKnex() { + this.applyWhere() + return this.knexQuery + } } diff --git a/src/orm/relations/base/query_builder.ts b/src/orm/relations/base/query_builder.ts index f4b0e5e6..116c56f4 100644 --- a/src/orm/relations/base/query_builder.ts +++ b/src/orm/relations/base/query_builder.ts @@ -165,6 +165,14 @@ export abstract class BaseQueryBuilder return this } + /** + * Return knex query + */ + toKnex() { + this.applyConstraints() + return super.toKnex() + } + /** * Get query sql */ diff --git a/test/orm/model_query_builder.spec.ts b/test/orm/model_query_builder.spec.ts index e90ac83f..83b2fdfe 100644 --- a/test/orm/model_query_builder.spec.ts +++ b/test/orm/model_query_builder.spec.ts @@ -10,7 +10,7 @@ import { test } from '@japa/runner' import { AppFactory } from '@adonisjs/core/factories/app' -import { column } from '../../src/orm/decorators/index.js' +import { column, hasMany } from '../../src/orm/decorators/index.js' import { scope } from '../../src/orm/base_model/index.js' import { ModelQueryBuilder } from '../../src/orm/query_builder/index.js' import { @@ -21,6 +21,7 @@ import { resetTables, getBaseModel, } from '../../test-helpers/index.js' +import { HasMany } from '../../src/types/relations.js' test.group('Model query builder', (group) => { group.setup(async () => { @@ -443,4 +444,51 @@ test.group('Model query builder', (group) => { const users = await User.query().count('* as total') assert.equal(Number(users[0].$extras.total), 2) }) + + test('apply relationship constraints when using sub query', async ({ fs, assert }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + const adapter = ormAdapter(db) + const BaseModel = getBaseModel(adapter) + + class Post extends BaseModel { + @column() + declare userId: number | null + + @column() + declare title: string + } + + class User extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare username: string + + @hasMany(() => Post) + declare posts: HasMany + } + + Post.boot() + User.boot() + + const users = await User.createMany([ + { + username: 'virk', + }, + { + username: 'nikk', + }, + ]) + + for (let user of users) { + await user.related('posts').create({ title: 'Test' }) + } + + const posts = await Post.query().whereIn('id', users[0].related('posts').query().select('id')) + + assert.lengthOf(posts, 1) + }) }) From 45e43c18f4818e9c545cbad86c264d487eef48fa Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 8 Aug 2024 10:41:19 +0530 Subject: [PATCH 70/73] chore: update dependencies --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 273d33c5..59a965d1 100644 --- a/package.json +++ b/package.json @@ -64,16 +64,16 @@ "@adonisjs/eslint-config": "^1.3.0", "@adonisjs/prettier-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", - "@commitlint/cli": "^19.3.0", + "@commitlint/cli": "^19.4.0", "@commitlint/config-conventional": "^19.2.2", "@japa/assert": "^3.0.0", "@japa/file-system": "^2.3.0", "@japa/runner": "^3.1.4", "@libsql/sqlite3": "^0.3.1", - "@swc/core": "^1.7.1", + "@swc/core": "^1.7.6", "@types/chance": "^1.1.6", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.12", + "@types/node": "^22.1.0", "@types/pretty-hrtime": "^1.0.3", "@types/qs": "^6.9.15", "@vinejs/vine": "^2.1.0", @@ -87,9 +87,9 @@ "eslint": "^8.57.0", "fs-extra": "^11.2.0", "github-label-sync": "^2.3.1", - "husky": "^9.1.1", - "luxon": "^3.4.4", - "mysql2": "^3.10.3", + "husky": "^9.1.4", + "luxon": "^3.5.0", + "mysql2": "^3.11.0", "np": "^10.0.7", "pg": "^8.12.0", "prettier": "^3.3.3", @@ -111,7 +111,7 @@ "knex": "^3.1.0", "knex-dynamic-connection": "^3.2.0", "pretty-hrtime": "^1.0.3", - "qs": "^6.12.3", + "qs": "^6.13.0", "slash": "^5.1.0", "tarn": "^3.0.2" }, From 0699d06c40aacae8cdbea482b6593852e7abf7f2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 8 Aug 2024 11:02:00 +0530 Subject: [PATCH 71/73] fix: pin strtok3 version --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 59a965d1..a10e4ef4 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,9 @@ "bugs": { "url": "https://github.com/adonisjs/lucid/issues" }, + "overrides": { + "strtok3": "8.0.1" + }, "eslintConfig": { "extends": "@adonisjs/eslint-config/package" }, From 2fd32fc25f8740e44d811b15f27767cecb9e9fbe Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 8 Aug 2024 11:17:30 +0530 Subject: [PATCH 72/73] chore(release): 21.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a10e4ef4..c93f2f6d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/lucid", "description": "SQL ORM built on top of Active Record pattern", - "version": "21.1.1", + "version": "21.2.0", "engines": { "node": ">=18.16.0" }, From cbbb4fd5b4387970b1513ef6cf7721f44b2282dc Mon Sep 17 00:00:00 2001 From: Mohammd Siddiqui Date: Sat, 31 Aug 2024 14:07:16 +0100 Subject: [PATCH 73/73] feat: export Adapter from orm (#1030) --- src/orm/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/orm/main.ts b/src/orm/main.ts index f9e55aa7..646a9237 100644 --- a/src/orm/main.ts +++ b/src/orm/main.ts @@ -15,3 +15,4 @@ export { ModelQueryBuilder } from './query_builder/index.js' export { SnakeCaseNamingStrategy } from './naming_strategies/snake_case.js' export { CamelCaseNamingStrategy } from './naming_strategies/camel_case.js' export { Preloader } from './preloader/index.js' +export { Adapter } from './adapter/index.js'