Skip to content

Commit

Permalink
Merge pull request #110 from csse-uoft/unique-ids
Browse files Browse the repository at this point in the history
Unique identifier for instances
  • Loading branch information
LesterLyu authored Feb 20, 2024
2 parents d42bdd8 + cacdcd2 commit 718cafb
Show file tree
Hide file tree
Showing 17 changed files with 296 additions and 110 deletions.
6 changes: 4 additions & 2 deletions backend/loaders/graphDB.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ const {UpdateQueryPayload} = require('graphdb').query;
const {graphdb, mongodb} = require('../config');
const {sleep} = require('../utils');
const {namespaces} = require('./namespaces');
const {initGraphDB, MongoDBIdGenerator, importRepositorySnapshot} = require("graphdb-utils");
const {initGraphDB, UUIDGenerator, importRepositorySnapshot} = require("graphdb-utils");

let repository;

async function getRepository() {
Expand Down Expand Up @@ -452,7 +453,7 @@ INSERT DATA {

async function load() {
// ID Generator for creating new instances
const idGenerator = new MongoDBIdGenerator(mongodb.addr);
const idGenerator = new UUIDGenerator();

const result = await initGraphDB({
idGenerator,
Expand All @@ -464,6 +465,7 @@ async function load() {
namespaces,
// The repository name, a new repository will be created if it does not exist.
repositoryName: process.env.test ? "snmiTest" : "snmi",
debug: false
});

repository = result.repository;
Expand Down
231 changes: 231 additions & 0 deletions backend/migrations/2024-1-29-uuid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Run the migration script independently, ensuring the backend server is shut down beforehand.
// node ./backend/migrations/2024-1-29-uuid.js

require('dotenv').config()
const {GraphDB, UUIDGenerator, Transaction, SPARQL} = require("graphdb-utils");
const {db: mongoDB} = require("../loaders/mongoDB");
const {MDBDynamicFormModel} = require("../models/dynamicForm");
const {load} = require("../loaders/graphDB");
const crypto = require("crypto");
const {MDBUsageModel} = require("../models/usage");

async function getAllEntitiesWithId() {
const uris = new Set();
await GraphDB.sendSelectQuery(`
PREFIX sesame: <http://www.openrdf.org/schema/sesame#>
SELECT DISTINCT * FROM sesame:nil WHERE {
SELECT ?s WHERE {
{?s ?p ?o .}
UNION
{?s2 ?p2 ?s}
FILTER (ISIRI(?s))
FILTER (regex(str(?s), "_\\\\d+$")) # Ends with _ + number
}
} order by ?s
`, false, ({s}) => {
uris.add(s.value);
});
return uris;
}

async function migration_uuid() {

// Get all entities
const uris = await getAllEntitiesWithId();

// old uri -> new uri
const uriMap = new Map();
for (const uri of uris) {
const [name, id] = uri.split('_');
uriMap.set(uri, name + '_' + crypto.randomUUID());
}

for (const uri of uris) {
await renameEntity(uri, uriMap);
}
const entitiesLeft = await getAllEntitiesWithId();
if (entitiesLeft.size > 0) {
console.log(entitiesLeft)
throw new Error(`Something wrong happened, not all entities are renamed (${entitiesLeft.size})`);
} else {
return uriMap;
}

}

// Get all relative triples and rename it.
async function renameEntity(uri, uriMap) {
// console.log('Renaming', uri);

// Get all relative triples that uses `uri`
const triples = [];
await GraphDB.sendConstructQuery(`
PREFIX sesame: <http://www.openrdf.org/schema/sesame#>
PREFIX : <http://snmi#>
CONSTRUCT {
<${uri}> ?p ?o.
?s2 ?p2 <${uri}>
} FROM sesame:nil WHERE {
<${uri}> ?p ?o .
Optional {
?s2 ?p2 <${uri}>
}
}`, ({subject, predicate, object}) => {
triples.push([subject, predicate, object]);
});
//
// console.log(triples)

// Rename the triples
const deleteTriples = [];
const inserTriples = [];
for (const [s, p, o] of triples) {
let shouldUpdate = false;
const insertTriple = [s, p, o];
if (uriMap.has(s.value)) {
insertTriple[0] = {value: uriMap.get(s.value)};
shouldUpdate = true;
}
if (o.termType === 'NamedNode' && uriMap.has(o.value)) {
insertTriple[2] = {value: uriMap.get(o.value)};
shouldUpdate = true;
}
if (shouldUpdate) {
let objectPart;
if (o.termType === "Literal") {
const dataType = o.datatype.value;
objectPart = `${JSON.stringify(o.value)}^^<${dataType}>`
} else {
// IRI
objectPart = `<${o.value}>`;
}

// Delete
deleteTriples.push(`<${s.value}> <${p.value}> ${objectPart}.`);

// Insert
inserTriples.push(`<${insertTriple[0].value}> <${insertTriple[1].value}> ${o.termType === 'Literal' ? objectPart : `<${insertTriple[2].value}>`}.`);
}
}

if (deleteTriples.length === 0 && inserTriples.length === 0) return;

const query = `DELETE WHERE {\n\t${deleteTriples.join('\n\t')}\n};\nINSERT DATA {\n\t${inserTriples.join('\n\t')}\n}`;
// console.log(query)
await GraphDB.sendUpdateQuery(query);
}

function updateInstance(uriMap, field) {
if (!field._uri) return;

// Update _id, _uri, id, iri, based on _uri
const uri = SPARQL.ensureFullURI(field._uri);
if (uri && uriMap.has(uri)) {
field._uri = uriMap.get(uri);
const id = field._uri.split('_').slice(-1)[0];
if (field._id) field._id = id;
if (field.id) field.id = id;
if (field.iri) field.iri = uri;
}
}

async function migrateFormStructure(uriMap) {
const forms = await MDBDynamicFormModel.find({});
for (const form of forms) {
const oldCreatedBy = SPARQL.ensureFullURI(form.createdBy);
if (uriMap.has(oldCreatedBy)) {
form.createdBy = SPARQL.ensurePrefixedURI(uriMap.get(oldCreatedBy));
}
for (const step of form.formStructure) {
for (const field of step.fields) {
updateInstance(uriMap, field);

const implementation = field.implementation;
if (implementation) {
updateInstance(uriMap, implementation);
updateInstance(uriMap, implementation.fieldType);
if (implementation.options) {
for (const option of implementation.options) {
updateInstance(uriMap, option)
}
}
} else if (field.type === 'question') {
const uri = `:question_${field.id}`;
if (uriMap.has(SPARQL.ensureFullURI(uri))) {
field.id = uriMap.get(SPARQL.ensureFullURI(uri)).split('_').slice(-1)[0];
}
} else {
throw Error("Should not reach here.")
}


}
}
form.markModified('formStructure');
await form.save();
// console.log(JSON.stringify(form.toJSON(), null, 2));
}

}

async function migrateUsages(uriMap) {
const usages = await MDBUsageModel.find({});
for (const usage of usages) {
const {optionKeys, option, genericType} = usage;
const newOptionKeys = [];
for (const id of optionKeys) {
const uri = SPARQL.ensureFullURI(`:${option}_${id}`);
if (uriMap.has(uri)) {
const newUri = uriMap.get(uri);
newOptionKeys.push(newUri.split('_').slice(-1)[0]);
} else {
newOptionKeys.push(id);
}
}
usage.optionKeys = newOptionKeys;
await usage.save();
}
}

async function main() {
await load();

await Transaction.beginTransaction();

const session = await mongoDB.startSession();
session.startTransaction();

let uriMap;
try {
// GraphDB
uriMap = await migration_uuid();

// MongoDB
await migrateFormStructure(uriMap);
await migrateUsages(uriMap);

// commit
await Transaction.commit();
session.commitTransaction();
session.endSession();

console.log('UUID Migration done.')
process.exit(0);

} catch (e) {
console.error(e);
console.log('UUID Migration failed. Rolling back...')
await Transaction.rollback();
process.exit(1);
}
}
if (!module.parent) {
main().catch(console.error);

} else {
throw new Error(`You should not import this file, this is meant to be run standalone.`);
}

6 changes: 6 additions & 0 deletions backend/migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## DB Migrations
[2024-1-29-uuid.js](2024-1-29-uuid.js): Migrate GraphDB data from using incremental ID to UUID.
Run the migration script independently, ensuring the backend server is shut down beforehand.
```shell
node ./backend/migrations/2024-1-29-uuid.js
```
34 changes: 0 additions & 34 deletions backend/services/address/misc.js

This file was deleted.

5 changes: 2 additions & 3 deletions backend/services/address/streetDirections.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {GDBStreetType} = require('../../models/address');
const {GraphDB} = require('graphdb-utils')
const {randomUUID} = require("crypto");


// https://www150.statcan.gc.ca/n1/pub/92-500-g/2013001/tbl/tbl4-3-eng.htm
Expand Down Expand Up @@ -36,10 +37,8 @@ async function initStreetDirections() {
}
`);
const triples = [];
let cnt = 1;
for (const type of streetDirections) {
triples.push(`:streetDirection_${cnt} rdf:type owl:NamedIndividual, ic:StreetDirection;\n\t rdfs:label "${type}".`);
cnt++;
triples.push(`:streetDirection_${randomUUID()} rdf:type owl:NamedIndividual, ic:StreetDirection;\n\t rdfs:label "${type}".`);
}
await GraphDB.sendUpdateQuery(`
PREFIX ic: <http://ontology.eil.utoronto.ca/tove/icontact#>
Expand Down
5 changes: 2 additions & 3 deletions backend/services/address/streetTypes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {GDBStreetType} = require('../../models/address');
const {GraphDB} = require('graphdb-utils')
const {randomUUID} = require("crypto");


// https://www150.statcan.gc.ca/n1/pub/92-500-g/2013001/tbl/tbl4-2-eng.htm
Expand Down Expand Up @@ -46,10 +47,8 @@ async function initStreetTypes() {
}
`);
const triples = [];
let cnt = 1;
for (const type of streetTypes) {
triples.push(`:streetType_${cnt} rdf:type owl:NamedIndividual, ic:StreetType;\n\t rdfs:label "${type}".`);
cnt++;
triples.push(`:streetType_${randomUUID()} rdf:type owl:NamedIndividual, ic:StreetType;\n\t rdfs:label "${type}".`);
}
await GraphDB.sendUpdateQuery(`
PREFIX ic: <http://ontology.eil.utoronto.ca/tove/icontact#>
Expand Down
15 changes: 9 additions & 6 deletions backend/services/dynamicForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,14 @@ async function getIndividualsInClass(className) {
OPTIONAL {?s foaf:familyName ?familyName. ?s foaf:givenName ?givenName. } # For Person/Client
OPTIONAL {?s :hasType ?type . } # for needSatisfier
OPTIONAL {?s tove_org:hasName ?toveHasName . } # for tove_org:hasName property
OPTIONAL {?s :hasPrimaryEmail ?primaryEmail . } # for :hasPrimaryEmail property
FILTER (isIRI(?s))
}`;

// todo: volunteer will only give last name
await GraphDB.sendSelectQuery(query, false, ({s, label, labelEn, labelFr, name, familyName, givenName, type, lastName, toveHasName}) => {
if (labelEn?.value || labelFr?.value || label?.value || name?.value || (familyName?.value || givenName?.value) || type?.value || lastName?.value || toveHasName?.value) {
instances[s.id] = labelEn?.value || labelFr?.value || label?.value || name?.value || lastName?.value || type?.value || toveHasName?.value || `${familyName?.value || ''}, ${givenName?.value || ''}`;
await GraphDB.sendSelectQuery(query, false, ({s, label, labelEn, labelFr, name, familyName, givenName, type, lastName, toveHasName, primaryEmail}) => {
if (labelEn?.value || labelFr?.value || label?.value || name?.value || (familyName?.value || givenName?.value) || type?.value || lastName?.value || toveHasName?.value || primaryEmail?.value) {
instances[s.id] = labelEn?.value || labelFr?.value || label?.value || name?.value || lastName?.value || type?.value || toveHasName?.value || primaryEmail?.value || `${familyName?.value || ''}, ${givenName?.value || ''}`;
} else {
instances[s.id] = SPARQL.ensurePrefixedURI(s.id) || s.id;
}
Expand Down Expand Up @@ -207,13 +208,15 @@ async function getURILabel(req, res) {
OPTIONAL {?s :hasVolunteer [foaf:familyName ?lastName] .} # For Service Provider: volunteer
OPTIONAL {?s foaf:familyName ?familyName. ?s foaf:givenName ?givenName. } # For Person/Client
OPTIONAL {?s :hasType ?type . } # for needSatisfier
OPTIONAL {?s tove_org:hasName ?toveHasName . } # for tove_org:hasName property
OPTIONAL {?s :hasPrimaryEmail ?primaryEmail . } # for :hasPrimaryEmail property
FILTER (isIRI(?s))
}`;

let result = ''
await GraphDB.sendSelectQuery(query, false, ({s, label, labelEn, labelFr, label2, name, familyName, givenName, type, lastName}) => {
if (labelEn?.value || labelFr?.value || label?.value || label2?.value || name?.value || (familyName?.value || givenName?.value) || type?.value || lastName?.value) {
result = labelEn?.value || labelFr?.value || label?.value || label2?.value || name?.value || lastName?.value || type?.value || `${familyName?.value || ''}, ${givenName?.value || ''}`;
await GraphDB.sendSelectQuery(query, false, ({s, label, labelEn, labelFr, label2, name, familyName, givenName, type, lastName, toveHasName, primaryEmail}) => {
if (labelEn?.value || labelFr?.value || label?.value || label2?.value || name?.value || (familyName?.value || givenName?.value) || type?.value || lastName?.value || toveHasName?.value || primaryEmail?.value) {
result = labelEn?.value || labelFr?.value || label?.value || label2?.value || name?.value || lastName?.value || type?.value || toveHasName?.value || primaryEmail?.value || `${familyName?.value || ''}, ${givenName?.value || ''}`;
} else {
result = SPARQL.ensurePrefixedURI(s.id) || s.id;
}
Expand Down
8 changes: 4 additions & 4 deletions backend/services/genericData/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,15 @@ const genericType2Populates = {
'serviceRegistration': ['address', 'needOccurrence', 'serviceOccurrence'],
'programRegistration' : ['address', 'needOccurrence', 'programOccurrence'],
'needOccurrence': ['address', 'occurrenceOf', 'client'],
'outcomeOccurrence': ['address'],
'outcomeOccurrence': ['address', 'occurrenceOf'],
'needSatisfierOccurrence': ['address'],
'clientAssessment': ['address', 'client'],
'referral': ['address', 'client'],
'service': ['serviceProvider.organization.address', 'serviceProvider.volunteer.address'],
'program': ['serviceProvider.organization.address', 'serviceProvider.volunteer.address', 'serviceProvider.organization', 'serviceProvider.volunteer', 'manager'],
'serviceOccurrence': ['address'],
'programOccurrence': ['address'],
'client': ['address'],
'serviceOccurrence': ['address', 'occurrenceOf'],
'programOccurrence': ['address', 'occurrenceOf'],
'client': ['address', 'needs'],
'appointment': ['address'],
'person': ['address'],
'volunteer': ['partnerOrganizations', 'organization'],
Expand Down
Loading

0 comments on commit 718cafb

Please sign in to comment.