Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate content versioning #22413

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions .changeset/loud-avocados-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---

Check warning on line 1 in .changeset/loud-avocados-serve.md

View workflow job for this annotation

GitHub Actions / Lint

File ignored by default.
'@directus/system-data': patch
'@directus/types': patch
'@directus/api': patch
---

Consolidated content versioning with a new `delta` field in `directus_versions`
16 changes: 10 additions & 6 deletions api/src/controllers/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,15 @@ router.get(

const { outdated, mainHash } = await service.verifyHash(version['collection'], version['item'], version['hash']);

const saves = await service.getVersionSavesById(version['id']);
let current;

const current = assign({}, ...saves);
if (version['delta']) {
current = version['delta'];
} else {
const saves = await service.getVersionSavesById(version['id']);

current = assign({}, ...saves);
}

const main = await service.getMainItem(version['collection'], version['item']);

Expand All @@ -236,11 +242,9 @@ router.post(

const mainItem = await service.getMainItem(version['collection'], version['item']);

await service.save(req.params['pk']!, req.body);

const saves = await service.getVersionSavesById(req.params['pk']!);
const updatedVersion = await service.save(req.params['pk']!, req.body);

const result = assign(mainItem, ...saves);
const result = assign(mainItem, updatedVersion['delta']);

res.locals['payload'] = { data: result || null };

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Knex } from 'knex';
import { assign, sortBy } from 'lodash-es';

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_versions', (table) => {
table.json('delta');
});

const versions = await knex.select('id').from('directus_versions');

for (const version of versions) {
const deltas = sortBy(
await knex.select('id', 'delta').from('directus_revisions').where('version', '=', version.id),
'id',
).map((revision) => (typeof revision.delta === 'string' ? JSON.parse(revision.delta) : revision.delta));

const finalVersionDelta = assign({}, ...deltas);

await knex('directus_versions')
.update({ delta: JSON.stringify(finalVersionDelta) })
.where('id', '=', version.id);
}
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_versions', (table) => {
table.dropColumn('delta');
});
}
33 changes: 28 additions & 5 deletions api/src/services/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export class VersionsService extends ItemsService {

if (!versions?.[0]) return null;

if (versions[0]['delta']) {
return [versions[0]['delta']];
}

const saves = await this.getVersionSavesById(versions[0]['id']);
Comment on lines +127 to 130
Copy link
Member

Choose a reason for hiding this comment

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

It feels strange, that this method now (in most/all cases?) returns only one item (the delta), especially while it is called getVersionSaves.
Do we even need that method and getVersionSavesById anymore? Can't we just always use the delta? I think the associated revision items now only serve the purpose of history, but not as source of truth anymore.

Copy link
Member Author

Choose a reason for hiding this comment

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

This getVersionSavesById is just for the edge case where a new version is created but on the older system when the migration is being run. All usages of it are part of the else clause where the delta is missing. 🤔

Copy link
Member

@paescuj paescuj May 9, 2024

Choose a reason for hiding this comment

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

Not sure if we should/can account for this edge case. I think this would mean we could never really get rid of it. In any case, I think this method needs some clean-up, for the first reason stated above.


return saves;
Expand Down Expand Up @@ -167,10 +171,11 @@ export class VersionsService extends ItemsService {
}

override async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]> {
// Only allow updates on "key" and "name" fields
// Only allow updates on "key", "name" and "delta" fields
Copy link
Member

Choose a reason for hiding this comment

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

I think we should handle delta updates inside the save method and don't allow direct updates, instead keeping updateMany for updates to key, name only.

Currently, would also do unnecessary duplicate work when called via save, fetching and merging getVersionSavesById two times.

If we should keep it here, reference docs need to be updated 1 where we would need to make clear what the differences are.

Footnotes

  1. https://docs.directus.io/reference/system/versions.html#update-multiple-content-versions

const versionUpdateSchema = Joi.object({
key: Joi.string(),
name: Joi.string().allow(null).optional(),
delta: Joi.object().optional(),
});

const { error } = versionUpdateSchema.validate(data);
Expand Down Expand Up @@ -253,17 +258,29 @@ export class VersionsService extends ItemsService {
delta: revisionDelta,
});

let existingDelta = version['delta'];

if (!existingDelta) {
const saves = await this.getVersionSavesById(key);

existingDelta = assign({}, ...saves);
}

const finalVersionDelta = assign({}, existingDelta, revisionDelta ? JSON.parse(revisionDelta) : null);

await this.updateOne(key, { delta: finalVersionDelta });
Copy link
Member

Choose a reason for hiding this comment

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

Saving a new version now requires update permission on directus_versions due to this call - we may want to disregard permissions here (need to check whether that is safe), make it a breaking change requiring update permissions, or find another solution

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm leaning towards the breaking change by requiring admins to update permissions. 🤔


const { cache } = getCache();

if (shouldClearCache(cache, undefined, collection)) {
cache.clear();
}

return data;
return finalVersionDelta;
}

async promote(version: PrimaryKey, mainHash: string, fields?: string[]) {
const { id, collection, item } = (await this.readOne(version)) as ContentVersion;
const { id, collection, item, delta } = (await this.readOne(version)) as ContentVersion;

// will throw an error if the accountability does not have permission to update the item
await this.authorizationService.checkAccess('update', collection, item);
Expand All @@ -276,9 +293,15 @@ export class VersionsService extends ItemsService {
});
}

const saves = await this.getVersionSavesById(id);
let versionResult;

const versionResult = assign({}, ...saves);
if (delta) {
versionResult = delta;
} else {
const saves = await this.getVersionSavesById(id);

versionResult = assign({}, ...saves);
}

const payloadToUpdate = fields ? pick(versionResult, fields) : versionResult;

Expand Down
4 changes: 4 additions & 0 deletions packages/system-data/src/fields/versions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ fields:
- field: user_updated
special:
- user-updated

- field: delta
special:
- cast-json
1 change: 1 addition & 0 deletions packages/types/src/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export type ContentVersion = {
date_updated: string | null;
user_created: string | null;
user_updated: string | null;
delta: Record<string, any> | null;
};
1 change: 1 addition & 0 deletions sdk/src/schema/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export type DirectusVersion<Schema> = MergeCoreCollection<
date_updated: 'datetime' | null;
user_created: DirectusUser<Schema> | string | null;
user_updated: DirectusUser<Schema> | string | null;
delta: Record<string, any> | null;
}
>;