Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const registerHook: HookConfig = async ({ action, init }, { env, services, datab

const schemaOptions = {
split: typeof env.SCHEMA_SYNC_SPLIT === 'boolean' ? env.SCHEMA_SYNC_SPLIT : true,
safe: !!env.SCHEMA_SYNC_SAFE,
};
Comment on lines 15 to 18
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schemaOptions now includes safe, but the CLI export-schema command later passes the raw args object (typed as { split: boolean }) into new SchemaExporter(...). After this change, that args object is missing the safe property and will not satisfy the constructor’s inferred option type under strict TS. Update the CLI path to merge defaults (e.g. pass { split: args.split, safe: schemaOptions.safe }) or change the constructor to accept partial options.

Copilot uses AI. Check for mistakes.

let schema: SchemaOverview | null;
Expand Down
38 changes: 35 additions & 3 deletions src/schemaExporter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
import type { ApiExtensionContext } from '@directus/extensions';
import type { Collection, ExtensionsServices, Snapshot, SnapshotField, SnapshotRelation } from '@directus/types';
import type { Collection, ExtensionsServices, Snapshot, SnapshotDiff, SnapshotField, SnapshotRelation } from '@directus/types';
import { mkdir, readFile, rm, writeFile } from 'fs/promises';
import { glob } from 'glob';
import { condenseAction } from './condenseAction.js';
import { exportHook } from './schemaExporterHooks.js';
import type { IExporter } from './types';
import { ExportHelper } from './utils.js';

/**
* Removes all destructive (DELETE) operations from a schema diff.
* Used with SCHEMA_SYNC_SAFE=true to prevent project-specific collections,
* fields, and relations from being dropped when importing a base snapshot.
*/
function filterNonDestructive(diff: SnapshotDiff): SnapshotDiff {
const isTopLevelDelete = (diffs: ReadonlyArray<{ kind: string; path?: unknown }>) =>
diffs.some(d => d.kind === 'D' && !d.path);

const deletedCollections = new Set(
diff.collections.filter(c => isTopLevelDelete(c.diff)).map(c => c.collection)
);

return {
...diff,
collections: diff.collections.filter(c => !deletedCollections.has(c.collection)),
fields: diff.fields.filter(
f => !deletedCollections.has(f.collection) && !isTopLevelDelete(f.diff)
),
systemFields: (diff.systemFields ?? []).filter(
f => !deletedCollections.has(f.collection) && !isTopLevelDelete(f.diff)
),
relations: diff.relations.filter(
r => !deletedCollections.has(r.collection) && !isTopLevelDelete(r.diff)
),
};
}

export class SchemaExporter implements IExporter {
protected _filePath: string;
protected _exportHandler = condenseAction(() => this.createAndSaveSnapshot());
Expand All @@ -15,7 +43,7 @@ export class SchemaExporter implements IExporter {
constructor(
protected getSchemaService: () => Promise<InstanceType<ExtensionsServices['SchemaService']>>,
protected logger: ApiExtensionContext['logger'],
protected options = { split: true }
protected options = { split: true, safe: false }
) {
Comment on lines 43 to 47
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SchemaExporter now expects an options object that includes safe, but several call sites (e.g. the CLI export-schema action) pass { split } only. With strict TS settings, this makes the constructor parameter type too narrow and will cause a compile error. Consider typing options as a partial (e.g. { split?: boolean; safe?: boolean }) and merging with defaults inside the constructor so callers can omit safe.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback and this feedback

this._filePath = `${ExportHelper.dataDir}/schema.json`;
}
Expand Down Expand Up @@ -112,8 +140,12 @@ export class SchemaExporter implements IExporter {
}

this.logger.info(`Diffing schema with hash: ${currentHash} and hash: ${hash}`);
const diff = await svc.diff(snapshot, { currentSnapshot, force: true });
let diff = await svc.diff(snapshot, { currentSnapshot, force: true });
if (diff !== null) {
if (this.options.safe) {
diff = filterNonDestructive(diff);
this.logger.info('SCHEMA_SYNC_SAFE: filtered destructive operations from diff');
}
this.logger.info(`Applying schema diff...`);
await svc.apply({ diff, hash: currentHash });
this.logger.info(`Schema updated`);
Expand Down