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
77 changes: 74 additions & 3 deletions acs-data-access/lib/api-v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,94 @@ export class APIv1 {
}


/** Finds every dataset whose stored config points at the given dataset.
*
* Reads the structure apps straight from ConfigDB rather than using the
* derived dataset map, so datasets that are currently invalid are still
* checked. An invalid dataset keeps its config document, so it can still
* be left holding a dangling reference.
*
* @param dataset_uuid The dataset that is about to be deleted.
* @returns {Promise<Array<{dataset: string, structure: string}>>}
*/
async find_referrers(dataset_uuid) {
const referrers = [];

for (const [structure, handler] of Object.entries(this.handlers)) {
const configs = await rx.firstValueFrom(this.cdb.search_app(structure));
if (!configs) continue;

for (const [uuid, config] of configs) {
if (uuid === dataset_uuid) continue;

if (handler.references(config, dataset_uuid))
referrers.push({ dataset: uuid, structure });
}
}

return referrers;
}

/** GET. Deletes a dataset.
*
* Refuses with 409 while another dataset still references this one, and
* returns the list of referrers so the caller can deal with them. See the
* comment below for why we refuse rather than rewrite the referrers.
*/
async delete_dataset(req, res){
const dataset_uuid = req.params.uuid;
if(!dataset_uuid) return fail(this.log, 422, `No req.params.uuid`);
if(!valid_uuid(dataset_uuid)) return fail(this.log, 422, `Invalid uuid ${dataset_uuid}`);

const ok = await this.auth.check_acl(
req.auth,
Constants.Perm.DeleteDataset,
dataset_uuid,
true,
);

if (!ok) return fail(this.log, 403, `You don't have DELETE permissions for ${dataset_uuid}`);

this.log(`Delete dataset called by ${req.auth} for ${dataset_uuid}`);

// remove all subclass relationships before deleting
/* Refuse the delete while anything still points here.
*
* The alternative is to edit the referrers. We do not, for three
* reasons. The caller holds DeleteDataset on this dataset only, so
* editing other datasets would change data they may have no permission
* to touch. A UnionComponents list can lose one entry and still mean
* something, but a SessionLimits dataset is a time window over its
* source, so removing the source leaves a window over nothing and there
* is no sensible repair. And refusing writes nothing at all, so a failed
* delete cannot leave the graph half updated.
*
* Callers delete from the top down: remove the union or session first,
* then its components. */
const referrers = await this.find_referrers(dataset_uuid);

if (referrers.length > 0) {
this.log(`Refusing to delete ${dataset_uuid}: referenced by %o`, referrers);

return res.status(409).json({
error: "dataset_in_use",
dataset: dataset_uuid,
message: `Dataset ${dataset_uuid} is still referenced by ${referrers.length} other dataset(s). Delete or update them first.`,
referrers,
});
}

/* Remove the links this dataset owns. The handler knows which way round
* its links point: a union is the superclass of its components, a
* session is a subclass of its source. */
const datasets = await rx.firstValueFrom(this.data.datasets);
const dataset = datasets.get(dataset_uuid);

if (dataset?.config && this.handlers[dataset.structure]) {
await this.handlers[dataset.structure]
.remove_subclass_relationships(dataset_uuid, dataset.config);
}

// remove any remaining subclass relationships before deleting
const subclasses = await this.cdb.class_direct_subclasses(dataset_uuid);
if(subclasses){
for(let s of subclasses){
Expand Down
12 changes: 12 additions & 0 deletions acs-data-access/lib/base-structure-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,16 @@ export class BaseStructureHandler {
resolve(ctx) {}
create_subclass_relationships(datasetUuid, config) {}
remove_subclass_relationships(datasetUuid, config) {}

/** Does this config point at the given dataset?
*
* Used by the delete path to find datasets that would be left with a
* dangling reference. Structures that only point at non-dataset objects
* return false.
*
* @param config The stored config document of another dataset.
* @param target_uuid The dataset UUID we are about to delete.
* @returns {boolean}
*/
references(config, target_uuid) { return false; }
}
6 changes: 6 additions & 0 deletions acs-data-access/lib/session-limits-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ export class SessionLimitsHandler extends BaseStructureHandler {
);
}

references(config, target_uuid) {
return config?.source === target_uuid;
}

async remove_subclass_relationships(dataset_uuid, config) {
if(!config?.source) return;

await this.cdb.class_remove_subclass(config.source, dataset_uuid);
this.log(`Removed ${dataset_uuid} from ${config.source} subclasses.`)
}
Expand Down
6 changes: 6 additions & 0 deletions acs-data-access/lib/sparkplug-sources-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export class SparkplugSourcesHandler extends BaseStructureHandler {
}];
}

/* A SparkplugSrc points at a device, not at another dataset, so it can
* never hold a dangling dataset reference. */
references() {
return false;
}

async create_subclass_relationships() {
return;
}
Expand Down
8 changes: 7 additions & 1 deletion acs-data-access/lib/unions-components-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@ export class UnionComponentsHandler extends BaseStructureHandler {
}
}

references(config, target_uuid) {
if(!Array.isArray(config)) return false;

return config.includes(target_uuid);
}

async remove_subclass_relationships(dataset_uuid, config) {
if(config.length <= 0) return;
if(!Array.isArray(config) || config.length <= 0) return;

for (const src of config){
await this.cdb.class_remove_subclass(dataset_uuid, src);
Expand Down
1 change: 1 addition & 0 deletions acs-data-access/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@influxdata/influxdb-client": "^1.35.0",
"deep-equal": "^2.2.3",
"express": "^5.0.1",
"immutable": "^5.0.0",
"p-limit": "^7.3.0",
"rxjs": "^7.8.2",
"stream": "^0.0.3",
Expand Down
Loading
Loading