diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e73a700d48..93e1fdc4dd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -153,10 +153,11 @@ jobs: api_ce_mysql: runs-on: ubuntu-latest needs: [lint, unit_back, unit_front] - name: '[CE] API Integration (mysql, node: ${{ matrix.node }})' + name: '[CE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }})' strategy: matrix: node: [14, 16, 18] + db_client: ['mysql', 'mysql2'] services: mysql: image: bitnami/mysql:latest @@ -186,15 +187,16 @@ jobs: - run: yarn install --immutable - uses: ./.github/actions/run-api-tests with: - dbOptions: '--dbclient=mysql --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi' + dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi' api_ce_mysql_5: runs-on: ubuntu-latest needs: [lint, unit_back, unit_front] - name: '[CE] API Integration (mysql:5 , node: ${{ matrix.node }})' + name: '[CE] API Integration (mysql:5, client: ${{ matrix.db_client }} , node: ${{ matrix.node }})' strategy: matrix: node: [14, 16, 18] + db_client: ['mysql', 'mysql2'] services: mysql: image: bitnami/mysql:5.7 @@ -223,12 +225,12 @@ jobs: - run: yarn install --immutable - uses: ./.github/actions/run-api-tests with: - dbOptions: '--dbclient=mysql --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi' + dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi' api_ce_sqlite: runs-on: ubuntu-latest needs: [lint, unit_back, unit_front] - name: '[CE] API Integration (sqlite: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})' + name: '[CE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})' strategy: matrix: node: [14, 16, 18] @@ -297,13 +299,14 @@ jobs: api_ee_mysql: runs-on: ubuntu-latest needs: [lint, unit_back, unit_front] - name: '[EE] API Integration (mysql, node: ${{ matrix.node }})' + name: '[EE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }})' if: github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]') env: STRAPI_LICENSE: ${{ secrets.strapiLicense }} strategy: matrix: node: [14, 16, 18] + db_client: ['mysql', 'mysql2'] services: mysql: image: bitnami/mysql:latest @@ -333,13 +336,13 @@ jobs: - run: yarn install --immutable - uses: ./.github/actions/run-api-tests with: - dbOptions: '--dbclient=mysql --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi' + dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi' runEE: true api_ee_sqlite: runs-on: ubuntu-latest needs: [lint, unit_back, unit_front] - name: '[EE] API Integration (sqlite: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})' + name: '[EE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})' if: github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]') env: STRAPI_LICENSE: ${{ secrets.strapiLicense }} diff --git a/examples/getstarted/config/database.js b/examples/getstarted/config/database.js index 88aa65cd8f..0ac5587d1c 100644 --- a/examples/getstarted/config/database.js +++ b/examples/getstarted/config/database.js @@ -28,6 +28,17 @@ const mysql = { }, }; +const mysql2 = { + client: 'mysql2', + connection: { + database: 'strapi', + user: 'strapi', + password: 'strapi', + port: 3306, + host: 'localhost', + }, +}; + const mariadb = { client: 'mysql', connection: { @@ -41,6 +52,7 @@ const mariadb = { const db = { mysql, + mysql2, sqlite, postgres, mariadb, diff --git a/examples/getstarted/package.json b/examples/getstarted/package.json index 551430435a..d15078c580 100644 --- a/examples/getstarted/package.json +++ b/examples/getstarted/package.json @@ -27,6 +27,7 @@ "better-sqlite3": "8.0.1", "lodash": "4.17.21", "mysql": "2.18.1", + "mysql2": "3.2.0", "passport-google-oauth2": "0.2.0", "pg": "8.8.0", "react": "^17.0.2", diff --git a/packages/core/admin/server/tests/admin-api-token-crud.test.api.js b/packages/core/admin/server/tests/admin-api-token-crud.test.api.js index ae030617d5..8f7d0654d5 100644 --- a/packages/core/admin/server/tests/admin-api-token-crud.test.api.js +++ b/packages/core/admin/server/tests/admin-api-token-crud.test.api.js @@ -8,6 +8,8 @@ const constants = require('../services/constants'); describe('Admin API Token v2 CRUD (api)', () => { let rq; let strapi; + let now; + let nowSpy; const deleteAllTokens = async () => { const tokens = await strapi.admin.services['api-token'].list(); @@ -22,6 +24,9 @@ describe('Admin API Token v2 CRUD (api)', () => { beforeAll(async () => { strapi = await createStrapiInstance(); rq = await createAuthRequest({ strapi }); + // To eliminate latency in the request and predict the expiry timestamp, we freeze Date.now() + now = Date.now(); + nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => now); // delete tokens await deleteAllTokens(); @@ -29,9 +34,14 @@ describe('Admin API Token v2 CRUD (api)', () => { // Cleanup actions afterAll(async () => { + nowSpy.mockRestore(); await strapi.destroy(); }); + afterEach(async () => { + await deleteAllTokens(); + }); + // create a predictable valid token that we can test with (delete, list, etc) let currentTokens = 0; const createValidToken = async (token = {}) => { @@ -178,9 +188,6 @@ describe('Admin API Token v2 CRUD (api)', () => { }); test('Creates a token with a 7-day lifespan', async () => { - const now = Date.now(); - jest.useFakeTimers('modern').setSystemTime(now); - const body = { name: 'api-token_tests-lifespan7', description: 'api-token_tests-description', @@ -217,9 +224,6 @@ describe('Admin API Token v2 CRUD (api)', () => { }); test('Creates a token with a 30-day lifespan', async () => { - const now = Date.now(); - jest.useFakeTimers('modern').setSystemTime(now); - const body = { name: 'api-token_tests-lifespan30', description: 'api-token_tests-description', @@ -256,9 +260,6 @@ describe('Admin API Token v2 CRUD (api)', () => { }); test('Creates a token with a 90-day lifespan', async () => { - const now = Date.now(); - jest.useFakeTimers('modern').setSystemTime(now); - const body = { name: 'api-token_tests-lifespan90', description: 'api-token_tests-description', diff --git a/packages/core/admin/server/tests/admin-transfer-token-crud.test.api.js b/packages/core/admin/server/tests/admin-transfer-token-crud.test.api.js index 33822b2c9b..66067c82f5 100644 --- a/packages/core/admin/server/tests/admin-transfer-token-crud.test.api.js +++ b/packages/core/admin/server/tests/admin-transfer-token-crud.test.api.js @@ -8,6 +8,8 @@ const constants = require('../services/constants'); describe('Admin Transfer Token CRUD (api)', () => { let rq; let strapi; + let now; + let nowSpy; const FULL_ACCESS = ['push', 'pull']; @@ -24,6 +26,9 @@ describe('Admin Transfer Token CRUD (api)', () => { beforeAll(async () => { strapi = await createStrapiInstance(); rq = await createAuthRequest({ strapi }); + // To eliminate latency in the request and predict the expiry timestamp, we freeze Date.now() + now = Date.now(); + nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => now); // delete tokens await deleteAllTokens(); @@ -31,9 +36,14 @@ describe('Admin Transfer Token CRUD (api)', () => { // Cleanup actions afterAll(async () => { + nowSpy.mockRestore(); await strapi.destroy(); }); + afterEach(async () => { + await deleteAllTokens(); + }); + // create a predictable valid token that we can test with (delete, list, etc) let currentTokens = 0; const createValidToken = async (token = {}) => { @@ -125,9 +135,6 @@ describe('Admin Transfer Token CRUD (api)', () => { }); test('Creates a transfer token with a 7-day lifespan', async () => { - const now = Date.now(); - jest.useFakeTimers('modern').setSystemTime(now); - const body = { name: 'transfer-token_tests-lifespan7', description: 'transfer-token_tests-description', @@ -158,14 +165,9 @@ describe('Admin Transfer Token CRUD (api)', () => { // Datetime stored in some databases may lose ms accuracy, so allow a range of 2 seconds for timing edge cases expect(Date.parse(res.body.data.expiresAt)).toBeGreaterThan(now + body.lifespan - 2000); expect(Date.parse(res.body.data.expiresAt)).toBeLessThan(now + body.lifespan + 2000); - - jest.useRealTimers(); }); test('Creates a transfer token with a 30-day lifespan', async () => { - const now = Date.now(); - jest.useFakeTimers('modern').setSystemTime(now); - const body = { name: 'transfer-token_tests-lifespan30', description: 'transfer-token_tests-description', @@ -196,14 +198,9 @@ describe('Admin Transfer Token CRUD (api)', () => { // Datetime stored in some databases may lose ms accuracy, so allow a range of 2 seconds for timing edge cases expect(Date.parse(res.body.data.expiresAt)).toBeGreaterThan(now + body.lifespan - 2000); expect(Date.parse(res.body.data.expiresAt)).toBeLessThan(now + body.lifespan + 2000); - - jest.useRealTimers(); }); test('Creates a transfer token with a 90-day lifespan', async () => { - const now = Date.now(); - jest.useFakeTimers('modern').setSystemTime(now); - const body = { name: 'transfer-token_tests-lifespan90', description: 'transfer-token_tests-description', @@ -234,8 +231,6 @@ describe('Admin Transfer Token CRUD (api)', () => { // Datetime stored in some databases may lose ms accuracy, so allow a range of 2 seconds for timing edge cases expect(Date.parse(res.body.data.expiresAt)).toBeGreaterThan(now + body.lifespan - 2000); expect(Date.parse(res.body.data.expiresAt)).toBeLessThan(now + body.lifespan + 2000); - - jest.useRealTimers(); }); test('Creates a transfer token with a null lifespan', async () => { @@ -492,6 +487,8 @@ describe('Admin Transfer Token CRUD (api)', () => { }); test('Returns a 404 if the resource to update does not exist', async () => { + await deleteAllTokens(); + const body = { name: 'transfer-token_tests-updated-name', description: 'transfer-token_tests-updated-description', diff --git a/packages/core/database/lib/dialects/index.js b/packages/core/database/lib/dialects/index.js index a0b44e77cd..e13fb4d95a 100644 --- a/packages/core/database/lib/dialects/index.js +++ b/packages/core/database/lib/dialects/index.js @@ -1,5 +1,8 @@ 'use strict'; +/** + * Require our dialect-specific code + */ const getDialectClass = (client) => { switch (client) { case 'postgres': @@ -13,12 +16,34 @@ const getDialectClass = (client) => { } }; +/** + * Get the dialect of a database client + * + * @param {string} The client value from a project database configuration + * @returns {string} The dialect of that client + */ +const getDialectName = (client) => { + switch (client) { + case 'postgres': + return 'postgres'; + case 'mysql': + case 'mysql2': + return 'mysql'; + case 'sqlite': + case 'sqlite-legacy': + return 'sqlite'; + default: + throw new Error(`Unknown dialect ${client}`); + } +}; + const getDialect = (db) => { const { client } = db.config.connection; + const dialectName = getDialectName(client); - const constructor = getDialectClass(client); + const constructor = getDialectClass(dialectName); const dialect = new constructor(db); - dialect.client = client; + dialect.client = dialectName; return dialect; }; diff --git a/packages/generators/app/lib/resources/templates/database-templates/js/database.template b/packages/generators/app/lib/resources/templates/database-templates/js/database.template index 9a97b6cd15..08cfc683e0 100644 --- a/packages/generators/app/lib/resources/templates/database-templates/js/database.template +++ b/packages/generators/app/lib/resources/templates/database-templates/js/database.template @@ -26,6 +26,27 @@ module.exports = ({ env }) => { }, pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) }, }, + mysql2: { + connection: { + host: env('DATABASE_HOST', 'localhost'), + port: env.int('DATABASE_PORT', 3306), + database: env('DATABASE_NAME', 'strapi'), + user: env('DATABASE_USERNAME', 'strapi'), + password: env('DATABASE_PASSWORD', 'strapi'), + ssl: env.bool('DATABASE_SSL', false) && { + key: env('DATABASE_SSL_KEY', undefined), + cert: env('DATABASE_SSL_CERT', undefined), + ca: env('DATABASE_SSL_CA', undefined), + capath: env('DATABASE_SSL_CAPATH', undefined), + cipher: env('DATABASE_SSL_CIPHER', undefined), + rejectUnauthorized: env.bool( + 'DATABASE_SSL_REJECT_UNAUTHORIZED', + true + ), + }, + }, + pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) }, + }, postgres: { connection: { connectionString: env('DATABASE_URL'), diff --git a/packages/generators/app/lib/resources/templates/database-templates/mysql2.template b/packages/generators/app/lib/resources/templates/database-templates/mysql2.template new file mode 100644 index 0000000000..2650bcedf6 --- /dev/null +++ b/packages/generators/app/lib/resources/templates/database-templates/mysql2.template @@ -0,0 +1,8 @@ +# Database +DATABASE_CLIENT=<%= client %> +DATABASE_HOST=<%= connection.host %> +DATABASE_PORT=<%= connection.port %> +DATABASE_NAME=<%= connection.database %> +DATABASE_USERNAME=<%= connection.username %> +DATABASE_PASSWORD=<%= connection.password %> +DATABASE_SSL=<%= connection.ssl %> diff --git a/packages/generators/app/lib/resources/templates/database-templates/ts/database.template b/packages/generators/app/lib/resources/templates/database-templates/ts/database.template index c1064bf683..638492157e 100644 --- a/packages/generators/app/lib/resources/templates/database-templates/ts/database.template +++ b/packages/generators/app/lib/resources/templates/database-templates/ts/database.template @@ -26,6 +26,27 @@ export default ({ env }) => { }, pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) }, }, + mysql2: { + connection: { + host: env('DATABASE_HOST', 'localhost'), + port: env.int('DATABASE_PORT', 3306), + database: env('DATABASE_NAME', 'strapi'), + user: env('DATABASE_USERNAME', 'strapi'), + password: env('DATABASE_PASSWORD', 'strapi'), + ssl: env.bool('DATABASE_SSL', false) && { + key: env('DATABASE_SSL_KEY', undefined), + cert: env('DATABASE_SSL_CERT', undefined), + ca: env('DATABASE_SSL_CA', undefined), + capath: env('DATABASE_SSL_CAPATH', undefined), + cipher: env('DATABASE_SSL_CIPHER', undefined), + rejectUnauthorized: env.bool( + 'DATABASE_SSL_REJECT_UNAUTHORIZED', + true + ), + }, + }, + pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) }, + }, postgres: { connection: { connectionString: env('DATABASE_URL'), diff --git a/packages/generators/app/lib/utils/db-client-dependencies.js b/packages/generators/app/lib/utils/db-client-dependencies.js index 6f72034cbb..d061879eab 100644 --- a/packages/generators/app/lib/utils/db-client-dependencies.js +++ b/packages/generators/app/lib/utils/db-client-dependencies.js @@ -2,6 +2,7 @@ const sqlClientModule = { mysql: { mysql: '2.18.1' }, + mysql2: { mysql2: '3.2.0' }, postgres: { pg: '8.8.0' }, sqlite: { 'better-sqlite3': '8.0.1' }, 'sqlite-legacy': { sqlite3: '^5.0.2' }, @@ -11,16 +12,11 @@ const sqlClientModule = { * Client dependencies */ module.exports = ({ client }) => { - switch (client) { - case 'sqlite': - case 'sqlite-legacy': - case 'postgres': - case 'mysql': - return { - ...sqlClientModule[client], - }; - - default: - throw new Error(`Invalid client "${client}"`); + if (client in sqlClientModule) { + return { + ...sqlClientModule[client], + }; } + + throw new Error(`Invalid client "${client}"`); }; diff --git a/test/api.js b/test/api.js index 18ca66721e..5264bd881c 100644 --- a/test/api.js +++ b/test/api.js @@ -33,6 +33,16 @@ const databases = { password: 'strapi', }, }, + mysql2: { + client: 'mysql2', + connection: { + host: '127.0.0.1', + port: 3306, + database: 'strapi_test', + username: 'strapi', + password: 'strapi', + }, + }, sqlite: { client: 'sqlite', connection: { @@ -80,7 +90,7 @@ yargs }) .command( '$0', - 'run end to end tests', + 'run API integration tests', (yarg) => { yarg.option('database', { alias: 'db', diff --git a/yarn.lock b/yarn.lock index bcf6cc70ed..9ef59ef39a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14095,6 +14095,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -17008,6 +17015,15 @@ __metadata: languageName: node linkType: hard +"generate-function@npm:^2.3.1": + version: 2.3.1 + resolution: "generate-function@npm:2.3.1" + dependencies: + is-property: ^1.0.2 + checksum: 652f083de206ead2bae4caf9c7eeb465e8d98c0b8ed2a29c6afc538cef0785b5c6eea10548f1e13cc586d3afd796c13c830c2cb3dc612ec2457b2aadda5f57c9 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.1, gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -17202,6 +17218,7 @@ __metadata: better-sqlite3: 8.0.1 lodash: 4.17.21 mysql: 2.18.1 + mysql2: 3.2.0 passport-google-oauth2: 0.2.0 pg: 8.8.0 react: ^17.0.2 @@ -18523,7 +18540,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -19379,6 +19396,13 @@ __metadata: languageName: node linkType: hard +"is-property@npm:^1.0.2": + version: 1.0.2 + resolution: "is-property@npm:1.0.2" + checksum: 33b661a3690bcc88f7e47bb0a21b9e3187e76a317541ea7ec5e8096d954f441b77a46d8930c785f7fbf4ef8dfd624c25495221e026e50f74c9048fe501773be5 + languageName: node + linkType: hard + "is-regex@npm:^1.1.2, is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -21752,6 +21776,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^5.2.1": + version: 5.2.1 + resolution: "long@npm:5.2.1" + checksum: 9264da12d1b7df67e5aa6da4498144293caf1ad12e7f092efe4e9a2d32c53f0bbf7334f7cef997080a2a3af061142558ab366efa71698d98b1cdb883477445a7 + languageName: node + linkType: hard + "longest-streak@npm:^2.0.0": version: 2.0.4 resolution: "longest-streak@npm:2.0.4" @@ -21847,6 +21878,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^7.14.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 + languageName: node + linkType: hard + "lru_map@npm:^0.3.3": version: 0.3.3 resolution: "lru_map@npm:0.3.3" @@ -22980,6 +23018,22 @@ __metadata: languageName: node linkType: hard +"mysql2@npm:3.2.0": + version: 3.2.0 + resolution: "mysql2@npm:3.2.0" + dependencies: + denque: ^2.1.0 + generate-function: ^2.3.1 + iconv-lite: ^0.6.3 + long: ^5.2.1 + lru-cache: ^7.14.1 + named-placeholders: ^1.1.3 + seq-queue: ^0.0.5 + sqlstring: ^2.3.2 + checksum: 7885d3b4f575e7321754de14bb4fadb80d4705d5b00f8b645d760ae7a0f3170ac6d4fe6e7b9307e698fe84c3ee047d7c2652f01b95c9393f56eace993f2952ab + languageName: node + linkType: hard + "mysql@npm:2.18.1": version: 2.18.1 resolution: "mysql@npm:2.18.1" @@ -23003,6 +23057,15 @@ __metadata: languageName: node linkType: hard +"named-placeholders@npm:^1.1.3": + version: 1.1.3 + resolution: "named-placeholders@npm:1.1.3" + dependencies: + lru-cache: ^7.14.1 + checksum: 7834adc91e92ae1b9c4413384e3ccd297de5168bb44017ff0536705ddc4db421723bd964607849265feb3f6ded390f84cf138e5925f22f7c13324f87a803dc73 + languageName: node + linkType: hard + "nan@npm:^2.12.1": version: 2.16.0 resolution: "nan@npm:2.16.0" @@ -27778,6 +27841,13 @@ __metadata: languageName: node linkType: hard +"seq-queue@npm:^0.0.5": + version: 0.0.5 + resolution: "seq-queue@npm:0.0.5" + checksum: f8695a6cb613e1b378b9686cde4ea626944091a412fc1c9d24c5039283d4351dd115f4505e4cf103d3a2e4a9a6a72fc7698fdce703839fb1fec9627aa4ce5563 + languageName: node + linkType: hard + "serialize-javascript@npm:^4.0.0": version: 4.0.0 resolution: "serialize-javascript@npm:4.0.0" @@ -28482,6 +28552,13 @@ __metadata: languageName: node linkType: hard +"sqlstring@npm:^2.3.2": + version: 2.3.3 + resolution: "sqlstring@npm:2.3.3" + checksum: 1e7e2d51c38a0cf7372e875408ca100b6e0c9a941ab7773975ea41fb36e5528e404dc787689be855780cf6d0a829ff71027964ae3a05a7446e91dce26672fda7 + languageName: node + linkType: hard + "sshpk@npm:^1.7.0": version: 1.17.0 resolution: "sshpk@npm:1.17.0"