Skip to content

Commit 4072715

Browse files
committed
feat: add SCHEMA_SYNC_SAFE flag to prevent destructive schema imports
When SCHEMA_SYNC_SAFE=true, all DELETE operations are filtered from the schema diff before applying. This allows project-specific collections, fields and relations to coexist with a base schema snapshot without being dropped on import. Use case: multi-project setups where a base template schema is imported into projects that extend it with their own collections and fields. Made-with: Cursor
1 parent cdd94ef commit 4072715

2 files changed

Lines changed: 36 additions & 3 deletions

File tree

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const registerHook: HookConfig = async ({ action, init }, { env, services, datab
1414

1515
const schemaOptions = {
1616
split: typeof env.SCHEMA_SYNC_SPLIT === 'boolean' ? env.SCHEMA_SYNC_SPLIT : true,
17+
safe: !!env.SCHEMA_SYNC_SAFE,
1718
};
1819

1920
let schema: SchemaOverview | null;

src/schemaExporter.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
import type { Snapshot, SnapshotField, SnapshotRelation } from '@directus/api/dist/types';
22
import type { ApiExtensionContext } from '@directus/extensions';
3-
import type { Collection, ExtensionsServices } from '@directus/types';
3+
import type { Collection, ExtensionsServices, SnapshotDiff } from '@directus/types';
44
import { mkdir, readFile, rm, writeFile } from 'fs/promises';
55
import { glob } from 'glob';
66
import { condenseAction } from './condenseAction.js';
77
import { exportHook } from './schemaExporterHooks.js';
88
import type { IExporter } from './types';
99
import { ExportHelper } from './utils.js';
1010

11+
/**
12+
* Removes all destructive (DELETE) operations from a schema diff.
13+
* Used with SCHEMA_SYNC_SAFE=true to prevent project-specific collections,
14+
* fields, and relations from being dropped when importing a base snapshot.
15+
*/
16+
function filterNonDestructive(diff: SnapshotDiff): SnapshotDiff {
17+
const isTopLevelDelete = (diffs: ReadonlyArray<{ kind: string; path?: unknown }>) =>
18+
diffs.some(d => d.kind === 'D' && !d.path);
19+
20+
const deletedCollections = new Set(
21+
diff.collections.filter(c => isTopLevelDelete(c.diff)).map(c => c.collection)
22+
);
23+
24+
return {
25+
...diff,
26+
collections: diff.collections.filter(c => !deletedCollections.has(c.collection)),
27+
fields: diff.fields.filter(
28+
f => !deletedCollections.has(f.collection) && !isTopLevelDelete(f.diff)
29+
),
30+
systemFields: (diff.systemFields ?? []).filter(
31+
f => !deletedCollections.has(f.collection) && !isTopLevelDelete(f.diff)
32+
),
33+
relations: diff.relations.filter(
34+
r => !deletedCollections.has(r.collection) && !isTopLevelDelete(r.diff)
35+
),
36+
};
37+
}
38+
1139
export class SchemaExporter implements IExporter {
1240
protected _filePath: string;
1341
protected _exportHandler = condenseAction(() => this.createAndSaveSnapshot());
@@ -16,7 +44,7 @@ export class SchemaExporter implements IExporter {
1644
constructor(
1745
protected getSchemaService: () => Promise<InstanceType<ExtensionsServices['SchemaService']>>,
1846
protected logger: ApiExtensionContext['logger'],
19-
protected options = { split: true }
47+
protected options = { split: true, safe: false }
2048
) {
2149
this._filePath = `${ExportHelper.dataDir}/schema.json`;
2250
}
@@ -113,8 +141,12 @@ export class SchemaExporter implements IExporter {
113141
}
114142

115143
this.logger.info(`Diffing schema with hash: ${currentHash} and hash: ${hash}`);
116-
const diff = await svc.diff(snapshot, { currentSnapshot, force: true });
144+
let diff = await svc.diff(snapshot, { currentSnapshot, force: true });
117145
if (diff !== null) {
146+
if (this.options.safe) {
147+
diff = filterNonDestructive(diff);
148+
this.logger.info('SCHEMA_SYNC_SAFE: filtered destructive operations from diff');
149+
}
118150
this.logger.info(`Applying schema diff...`);
119151
await svc.apply({ diff, hash: currentHash });
120152
this.logger.info(`Schema updated`);

0 commit comments

Comments
 (0)