From 27bfe5c7fe9f44ef1b10157820e29c02626254a3 Mon Sep 17 00:00:00 2001 From: Gerard Date: Wed, 15 Nov 2023 15:29:45 +0100 Subject: [PATCH] Sort exports Fix merge option Add utils tests --- .gitignore | 1 + CHANGELOG.md | 9 +++- package-lock.json | 30 ++++++++++- package.json | 7 ++- src/collectionExporter.ts | 13 ++--- src/exportManager.ts | 2 +- src/utils.test.ts | 104 ++++++++++++++++++++++++++++++++++++++ src/utils.ts | 18 +++++++ tsconfig.json | 2 +- tsconfig.test.json | 30 +++++++++++ 10 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 src/utils.test.ts create mode 100644 tsconfig.test.json diff --git a/.gitignore b/.gitignore index 8d67a86..e0fb9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store node_modules dist +dist-test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 999914e..3e620ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -# Version 1.4.2 +# Version 1.5.0 ⚠️ + + - **Sorts exported object keys** + - Fixes issue where the order of the exported object keys would change between exports, causing unnecessary changes in git. + - **merge option fixed** + - The merge option was introduced in version 1.3.0, but it was not working as intended. This has now been fixed. + +## Version 1.4.2 - Add `import-schema` and `export-schema` commands to import/export only the schema. diff --git a/package-lock.json b/package-lock.json index 689e2b0..da5ea4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "directus-extension-schema-sync", - "version": "1.4.2", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "directus-extension-schema-sync", - "version": "1.4.2", + "version": "1.5.0", "devDependencies": { "@directus/extensions-sdk": "10.1.11", "@directus/types": "^10.1.6", + "@types/keyv": "^4.2.0", "@types/node": "^20.8.7", "typescript": "^5.2.2" } @@ -990,6 +991,16 @@ "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==", "dev": true }, + "node_modules/@types/keyv": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-4.2.0.tgz", + "integrity": "sha512-xoBtGl5R9jeKUhc8ZqeYaRDx04qqJ10yhhXYGmJ4Jr8qKpvMsDQQrNUvF/wUJ4klOtmJeJM+p2Xo3zp9uaC3tw==", + "deprecated": "This is a stub types definition. keyv provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "keyv": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", @@ -3030,6 +3041,12 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -3054,6 +3071,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/knex": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.2.tgz", diff --git a/package.json b/package.json index e0f19fa..835c4c4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "directus-extension-schema-sync", "description": "Sync schema and data betwreen Directus instances", "icon": "sync_alt", - "version": "1.4.2", + "version": "1.5.0", "repository": { "type": "git", "url": "https://github.com/bcc-code/directus-schema-sync.git" @@ -29,11 +29,14 @@ "scripts": { "build": "directus-extension build", "dev": "directus-extension build -w --no-minify", - "link": "directus-extension link" + "link": "directus-extension link", + "pre-test": "tsc -p tsconfig.test.json", + "test": "npm run pre-test && node --test dist-test/" }, "devDependencies": { "@directus/extensions-sdk": "10.1.11", "@directus/types": "^10.1.6", + "@types/keyv": "^4.2.0", "@types/node": "^20.8.7", "typescript": "^5.2.2" } diff --git a/src/collectionExporter.ts b/src/collectionExporter.ts index bd2c87d..0fa7644 100644 --- a/src/collectionExporter.ts +++ b/src/collectionExporter.ts @@ -3,7 +3,7 @@ import { ApiExtensionContext, Item, PrimaryKey, Query } from '@directus/types'; import { readFile, writeFile } from 'fs/promises'; import { condenseAction } from './condenseAction.js'; import type { CollectionExporterOptions, IExporter, IGetItemsService, ItemsService, JSONString } from './types'; -import { ExportHelper, getDiff } from './utils.js'; +import { ExportHelper, getDiff, sortObject } from './utils.js'; const DEFAULT_COLLECTION_EXPORTER_OPTIONS: CollectionExporterOptions = { excludeFields: [], @@ -109,7 +109,7 @@ class CollectionExporter implements IExporter { const itemsSvc = await this._getService(); const { query } = await this.settings(); - const items = await itemsSvc.readByQuery(query); + let items = await itemsSvc.readByQuery(query); if (!items.length) return ''; if (this.options.onExport) { @@ -118,10 +118,11 @@ class CollectionExporter implements IExporter { const alteredItem = await this.options.onExport(item, itemsSvc); if (alteredItem) alteredItems.push(alteredItem); } - return JSON.stringify(alteredItems, null, 2); - } else { - return JSON.stringify(items, null, 2); - } + + items = alteredItems; + } + + return JSON.stringify(sortObject(items), null, 2); } public async loadJSON(json: JSONString | null, merge = false) { diff --git a/src/exportManager.ts b/src/exportManager.ts index e9a88c4..33cacf3 100644 --- a/src/exportManager.ts +++ b/src/exportManager.ts @@ -27,7 +27,7 @@ export class ExportManager { // SECOND: Import if needed public async loadAll(merge = false) { - await this._loadNextExporter(0); + await this._loadNextExporter(0, merge); } protected async _loadNextExporter(i = 0, merge = false) { diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..cf03892 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,104 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { deepEqual, getDiff, sortObject } from "./utils.js"; + +describe('sortObject', () => { + it('should sort object keys alphabetically', () => { + const input = { c: 1, a: 2, b: 3 }; + const assertedOutput = { a: 2, b: 3, c: 1 }; + assert.deepStrictEqual(sortObject(input), assertedOutput); + }); + + it('should sort nested object keys alphabetically', () => { + const input = { c: 1, a: { d: 4, b: 3 }, e: 5 }; + const assertedOutput = { a: { b: 3, d: 4 }, c: 1, e: 5 }; + assert.deepStrictEqual(sortObject(input), assertedOutput); + }); + + it('should sort array elements recursively', () => { + const input = [{ c: 1, a: 2 }, { b: 3 }]; + const assertedOutput = [{ a: 2, c: 1 }, { b: 3 }]; + assert.deepStrictEqual(sortObject(input), assertedOutput); + }); + + it('should return input if it is not an object', () => { + assert.deepStrictEqual(sortObject(null as any), null); + assert.deepStrictEqual(sortObject(42 as any), 42); + assert.deepStrictEqual(sortObject('hello' as any), 'hello'); + }); +}); + +describe('getDiff', () => { + it('should return the entire new object if the old object is null', () => { + const newObj = { a: 1, b: 2 }; + const oldObj = null; + const assertedOutput = { a: 1, b: 2 }; + assert.deepStrictEqual(getDiff(newObj, oldObj), assertedOutput); + }); + + it('should return null if the new and old objects are equal', () => { + const newObj = { a: 1, b: 2 }; + const oldObj = { a: 1, b: 2 }; + assert.deepStrictEqual(getDiff(newObj, oldObj), null); + }); + + it('should return only the different properties between the new and old objects', () => { + const newObj = { a: 1, b: 2, c: 3 }; + const oldObj = { a: 1, b: 3, d: 4 }; + const assertedOutput = { b: 2, c: 3 }; + assert.deepStrictEqual(getDiff(newObj, oldObj), assertedOutput); + }); + + it('should handle nested objects', () => { + const newObj = { a: 1, b: { c: 2, d: 3 } }; + const oldObj = { a: 1, b: { c: 2, d: 4 } }; + const assertedOutput = { b: { d: 3 } }; + assert.deepStrictEqual(getDiff(newObj, oldObj), assertedOutput); + }); + + it('should handle arrays', () => { + const newObj = { a: 1, b: [1, 2, 3] }; + const oldObj = { a: 1, b: [1, 2, 4] }; + const assertedOutput = { b: [1, 2, 3] }; + assert.deepStrictEqual(getDiff(newObj, oldObj), assertedOutput); + }); +}); + +describe('deepEqual', () => { + it('should return true for equal objects', () => { + const obj1 = { a: 1, b: { c: 2 } }; + const obj2 = { a: 1, b: { c: 2 } }; + assert.strictEqual(deepEqual(obj1, obj2), true); + }); + + it('should return false for different objects', () => { + const obj1 = { a: 1, b: { c: 2 } }; + const obj2 = { a: 1, b: { c: 3 } }; + assert.strictEqual(deepEqual(obj1, obj2), false); + }); + + it('should return true for equal arrays', () => { + const arr1 = [1, 2, { a: 3 }]; + const arr2 = [1, 2, { a: 3 }]; + assert.strictEqual(deepEqual(arr1, arr2), true); + }); + + it('should return false for different arrays', () => { + const arr1 = [1, 2, { a: 3 }]; + const arr2 = [1, 2, { a: 4 }]; + assert.strictEqual(deepEqual(arr1, arr2), false); + }); + + it('should return true for equal primitives', () => { + assert.strictEqual(deepEqual(1, 1), true); + assert.strictEqual(deepEqual('hello', 'hello'), true); + assert.strictEqual(deepEqual(null, null), true); + assert.strictEqual(deepEqual(undefined, undefined), true); + }); + + it('should return false for different primitives', () => { + assert.strictEqual(deepEqual(1, 2), false); + assert.strictEqual(deepEqual('hello', 'world'), false); + assert.strictEqual(deepEqual(null, undefined), false); + }); +}); \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index ae0c436..d0c9084 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -109,3 +109,21 @@ export function getDiff(newObj: Record, oldObj: any) { }); return isDifferent ? result : null; } + +export function sortObject>(obj: T): T; +export function sortObject(obj: T[]): T[]; +export function sortObject | T[]>(obj: T): T { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(sortObject) as unknown as T; + } + + const sortedObj: Record = {}; + Object.keys(obj).sort().forEach(key => { + sortedObj[key] = sortObject((obj as Record)[key]); + }); + return sortedObj as T; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0a5ee6e..4d13576 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,4 +26,4 @@ "rootDir": "./src" }, "include": ["./src/**/*.ts"] -} +} \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..38e550a --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ES2022", + "moduleResolution": "node", + "strict": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "alwaysStrict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "resolveJsonModule": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "rootDir": "./src", + "outDir": "./dist-test", + }, + "include": ["src/utils.ts", "src/utils.test.ts"] +} \ No newline at end of file