Skip to content

Commit d947f87

Browse files
authored
Merge pull request #96 from alphanull/feat/schema-sync-safe
feat: add SCHEMA_SYNC_SAFE flag to prevent destructive schema imports
2 parents 5fd0ba7 + a03b559 commit d947f87

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,12 +1,40 @@
11
import type { ApiExtensionContext } from '@directus/extensions';
2-
import type { Collection, ExtensionsServices, Snapshot, SnapshotField, SnapshotRelation } from '@directus/types';
2+
import type { Collection, ExtensionsServices, Snapshot, SnapshotDiff, SnapshotField, SnapshotRelation } from '@directus/types';
33
import { mkdir, readFile, rm, writeFile } from 'fs/promises';
44
import { glob } from 'glob';
55
import { condenseAction } from './condenseAction.js';
66
import { exportHook } from './schemaExporterHooks.js';
77
import type { IExporter } from './types';
88
import { ExportHelper } from './utils.js';
99

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

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

0 commit comments

Comments
 (0)