diff --git a/package-lock.json b/package-lock.json index 84622b16..40b51ad2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28776,7 +28776,7 @@ }, "packages/cli": { "name": "directus-sync", - "version": "next", + "version": "3.1.5", "license": "GPL-3.0", "dependencies": { "@directus/sdk": "^17.0.1", diff --git a/packages/cli/src/lib/services/snapshot/snapshot-client.ts b/packages/cli/src/lib/services/snapshot/snapshot-client.ts index e91f2a82..f6ebc1a8 100644 --- a/packages/cli/src/lib/services/snapshot/snapshot-client.ts +++ b/packages/cli/src/lib/services/snapshot/snapshot-client.ts @@ -167,24 +167,50 @@ export class SnapshotClient { { path: INFO_JSON, content: info }, ]; - // Split collections + /* + * Split collections + * Folder and collections may have the same name (with different casing). + * Also, some file systems are case-insensitive. + */ + const existingCollections = new Set(); for (const collection of collections) { + const suffix = this.getSuffix(collection.collection, existingCollections); files.push({ - path: `${COLLECTIONS_DIR}/${collection.collection}.json`, + path: `${COLLECTIONS_DIR}/${collection.collection}${suffix}.json`, content: collection, }); } - // Split fields + + /* + * Split fields + * Groups and fields may have the same name (with different casing). + * Also, some file systems are case-insensitive. + * Therefore, inside a collection we have to deal with names conflicts. + */ + const existingFiles = new Set(); for (const field of fields) { + const suffix = this.getSuffix( + `${field.collection}/${field.field}`, + existingFiles, + ); files.push({ - path: `${FIELDS_DIR}/${field.collection}/${field.field}.json`, + path: `${FIELDS_DIR}/${field.collection}/${field.field}${suffix}.json`, content: field, }); } - // Split relations + + /* + * Split relations + * There should not be any conflicts here, but we still split them for consistency. + */ + const existingRelations = new Set(); for (const relation of relations) { + const suffix = this.getSuffix( + `${relation.collection}/${relation.field}`, + existingRelations, + ); files.push({ - path: `${RELATIONS_DIR}/${relation.collection}/${relation.field}.json`, + path: `${RELATIONS_DIR}/${relation.collection}/${relation.field}${suffix}.json`, content: relation, }); } @@ -192,6 +218,25 @@ export class SnapshotClient { return files; } + /** + * Get the suffix that should be added to the field name in order to avoid conflicts. + */ + protected getSuffix(baseName: string, existing: Set): string { + const base = baseName.toLowerCase(); // Some file systems are case-insensitive + let suffix = ''; + + if (existing.has(base)) { + let i = 2; + while (existing.has(`${base}_${i}`)) { + i++; + } + suffix = `_${i}`; + } + + existing.add(`${base}${suffix}`); + return suffix; + } + /** * Get the diff from Directus instance */ diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/dashboards.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/dashboards.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/dashboards.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/flows.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/flows.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/flows.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/folders.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/folders.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/folders.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/operations.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/operations.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/operations.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/panels.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/panels.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/panels.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/permissions.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/permissions.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/permissions.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/policies.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/policies.json new file mode 100644 index 00000000..e9ddc251 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/policies.json @@ -0,0 +1,34 @@ +[ + { + "name": "Administrator", + "icon": "verified", + "description": "$t:admin_description", + "ip_access": null, + "enforce_tfa": false, + "admin_access": true, + "app_access": true, + "roles": [ + { + "role": "_sync_default_admin_role", + "sort": null + } + ], + "_syncId": "_sync_default_admin_policy" + }, + { + "name": "$t:public_label", + "icon": "public", + "description": "$t:public_description", + "ip_access": null, + "enforce_tfa": false, + "admin_access": false, + "app_access": false, + "roles": [ + { + "role": null, + "sort": 1 + } + ], + "_syncId": "_sync_default_public_policy" + } +] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/presets.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/presets.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/presets.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/roles.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/roles.json new file mode 100644 index 00000000..4973cd84 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/roles.json @@ -0,0 +1,9 @@ +[ + { + "name": "Administrator", + "icon": "verified", + "description": "$t:admin_description", + "parent": null, + "_syncId": "_sync_default_admin_role" + } +] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/settings.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/settings.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/settings.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/translations.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/translations.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/collections/translations.json @@ -0,0 +1 @@ +[] diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/Profile_2.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/Profile_2.json new file mode 100644 index 00000000..edc6faf9 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/Profile_2.json @@ -0,0 +1,25 @@ +{ + "collection": "Profile", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "Profile", + "color": null, + "display_template": null, + "group": null, + "hidden": false, + "icon": "folder", + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": 1, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/directus_sync_id_map.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/directus_sync_id_map.json new file mode 100644 index 00000000..a2c04373 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/directus_sync_id_map.json @@ -0,0 +1,7 @@ +{ + "collection": "directus_sync_id_map", + "meta": null, + "schema": { + "name": "directus_sync_id_map" + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/profile.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/profile.json new file mode 100644 index 00000000..d874e201 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/collections/profile.json @@ -0,0 +1,28 @@ +{ + "collection": "profile", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "profile", + "color": null, + "display_template": null, + "group": null, + "hidden": false, + "icon": null, + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": null, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + }, + "schema": { + "name": "profile" + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/created_at.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/created_at.json new file mode 100644 index 00000000..edee32af --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/created_at.json @@ -0,0 +1,24 @@ +{ + "collection": "directus_sync_id_map", + "field": "created_at", + "type": "dateTime", + "meta": null, + "schema": { + "name": "created_at", + "table": "directus_sync_id_map", + "data_type": "datetime", + "default_value": "CURRENT_TIMESTAMP", + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": true, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/id.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/id.json new file mode 100644 index 00000000..0707090b --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/id.json @@ -0,0 +1,24 @@ +{ + "collection": "directus_sync_id_map", + "field": "id", + "type": "integer", + "meta": null, + "schema": { + "name": "id", + "table": "directus_sync_id_map", + "data_type": "integer", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": false, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": true, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/local_id.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/local_id.json new file mode 100644 index 00000000..6246f366 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/local_id.json @@ -0,0 +1,24 @@ +{ + "collection": "directus_sync_id_map", + "field": "local_id", + "type": "string", + "meta": null, + "schema": { + "name": "local_id", + "table": "directus_sync_id_map", + "data_type": "varchar", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/sync_id.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/sync_id.json new file mode 100644 index 00000000..7e27cb11 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/sync_id.json @@ -0,0 +1,24 @@ +{ + "collection": "directus_sync_id_map", + "field": "sync_id", + "type": "string", + "meta": null, + "schema": { + "name": "sync_id", + "table": "directus_sync_id_map", + "data_type": "varchar", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/table.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/table.json new file mode 100644 index 00000000..a59f5f50 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/directus_sync_id_map/table.json @@ -0,0 +1,24 @@ +{ + "collection": "directus_sync_id_map", + "field": "table", + "type": "string", + "meta": null, + "schema": { + "name": "table", + "table": "directus_sync_id_map", + "data_type": "varchar", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/Content_2.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/Content_2.json new file mode 100644 index 00000000..fa356a88 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/Content_2.json @@ -0,0 +1,25 @@ +{ + "collection": "profile", + "field": "Content", + "type": "alias", + "meta": { + "collection": "profile", + "conditions": null, + "display": null, + "display_options": null, + "field": "Content", + "group": null, + "hidden": false, + "interface": "group-raw", + "note": null, + "options": null, + "readonly": false, + "required": false, + "sort": 3, + "special": ["alias", "no-data", "group"], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/avatar_url.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/avatar_url.json new file mode 100644 index 00000000..75eb99e2 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/avatar_url.json @@ -0,0 +1,43 @@ +{ + "collection": "profile", + "field": "avatar_url", + "type": "string", + "meta": { + "collection": "profile", + "conditions": null, + "display": null, + "display_options": null, + "field": "avatar_url", + "group": "Content_2", + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "sort": 1, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "avatar_url", + "table": "profile", + "data_type": "varchar", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/content.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/content.json new file mode 100644 index 00000000..af039080 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/content.json @@ -0,0 +1,43 @@ +{ + "collection": "profile", + "field": "content", + "type": "string", + "meta": { + "collection": "profile", + "conditions": null, + "display": null, + "display_options": null, + "field": "content", + "group": "Content_2", + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "sort": 2, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "content", + "table": "profile", + "data_type": "varchar", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/id.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/id.json new file mode 100644 index 00000000..d006e539 --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/fields/profile/id.json @@ -0,0 +1,43 @@ +{ + "collection": "profile", + "field": "id", + "type": "integer", + "meta": { + "collection": "profile", + "conditions": null, + "display": null, + "display_options": null, + "field": "id", + "group": null, + "hidden": true, + "interface": "input", + "note": null, + "options": null, + "readonly": true, + "required": false, + "sort": 1, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "id", + "table": "profile", + "data_type": "integer", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": false, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": true, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/info.json b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/info.json new file mode 100644 index 00000000..b3b4df7e --- /dev/null +++ b/packages/e2e/dumps/sources/group-and-field-names-conflict/snapshot/info.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "directus": "11.2.1", + "vendor": "sqlite" +} diff --git a/packages/e2e/spec/entrypoint.spec.ts b/packages/e2e/spec/entrypoint.spec.ts index be783013..22601604 100644 --- a/packages/e2e/spec/entrypoint.spec.ts +++ b/packages/e2e/spec/entrypoint.spec.ts @@ -41,6 +41,7 @@ import { } from './operations/index.js'; import { updateDefaultData } from './default-data/index.js'; import { configPathInfo } from './config/index.js'; +import { groupAndFieldNamesConflict } from './snapshot/index.js'; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; @@ -101,5 +102,7 @@ describe('Tests entrypoint ->', () => { removePermissionDuplicates(context); pullAndPushPublicPermissions(context); + groupAndFieldNamesConflict(context); + removeTrackedItem(context); }); diff --git a/packages/e2e/spec/snapshot/group-and-field-names-conflict.ts b/packages/e2e/spec/snapshot/group-and-field-names-conflict.ts new file mode 100644 index 00000000..bf040773 --- /dev/null +++ b/packages/e2e/spec/snapshot/group-and-field-names-conflict.ts @@ -0,0 +1,40 @@ +import { Context } from '../helpers/index.js'; +import Path from 'path'; +import Fs from 'fs-extra'; + +export const groupAndFieldNamesConflict = (context: Context) => { + it('group and field with same name', async () => { + // -------------------------------------------------------------------- + // Init sync client and push + const syncInit = await context.getSync( + 'sources/group-and-field-names-conflict', + ); + await syncInit.push(); + + // Create another sync client and pull + const sync = await context.getSync('temp/group-and-field-names-conflict'); + await sync.pull(); + const dumpPath = sync.getDumpPath(); + + // -------------------------------------------------------------------- + // Get the files names in the snapshot folder + const collectionsPath = Path.join(dumpPath, 'snapshot', 'collections'); + const collectionsFiles = Fs.readdirSync(collectionsPath); + + expect(collectionsFiles).toHaveSize(3); + expect(collectionsFiles).toContain('directus_sync_id_map.json'); + expect(collectionsFiles).toContain('Profile.json'); + expect(collectionsFiles).toContain('profile_2.json'); + + // -------------------------------------------------------------------- + // Get the files names in the snapshot folder + const fieldsPath = Path.join(dumpPath, 'snapshot', 'fields', 'profile'); + const fieldsFiles = Fs.readdirSync(fieldsPath); + + expect(fieldsFiles).toHaveSize(4); + expect(fieldsFiles).toContain('id.json'); + expect(fieldsFiles).toContain('avatar_url.json'); + expect(fieldsFiles).toContain('Content.json'); + expect(fieldsFiles).toContain('content_2.json'); + }); +}; diff --git a/packages/e2e/spec/snapshot/index.ts b/packages/e2e/spec/snapshot/index.ts new file mode 100644 index 00000000..67df37bf --- /dev/null +++ b/packages/e2e/spec/snapshot/index.ts @@ -0,0 +1 @@ +export * from './group-and-field-names-conflict.js';