Skip to content

Commit ca92c4c

Browse files
authored
Move prep work from Forms to Datasets.createOrMerge (getodk#810)
* Refactoring Datasets.createOrUpdate * Combining form-related arguments in Datasets.createOrMerge * Removed extra dataset-fetching query
1 parent fd2c6ea commit ca92c4c

File tree

2 files changed

+62
-49
lines changed

2 files changed

+62
-49
lines changed

lib/model/query/datasets.js

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -85,44 +85,69 @@ const isNilOrEmpty = either(isNil, isEmpty);
8585
const _createOrMerge = (dataset, fields, acteeId, publish) => sql`
8686
${_insertDatasetDef(dataset, acteeId, true, publish)}
8787
${isNilOrEmpty(fields) ? sql`` : _insertProperties(fields, publish)}
88-
SELECT "action" FROM ds
88+
SELECT "action", "id" FROM ds
8989
`;
9090

91-
// Creates or merges dataset, properties and field mapping.
92-
// Expects dataset:Frame auxed with `formDefId`, `schemaId`,
93-
// and array of field:Frame auxed with `propertyName`
94-
const createOrMerge = (dataset, fields, publish = false) => ({ one, Actees, Datasets, Projects }) =>
95-
Promise.all([
96-
Projects.getById(dataset.projectId).then((o) => o.get()),
97-
Datasets.get(dataset.projectId, dataset.name)
98-
])
99-
.then(([project, datasetOption]) =>
100-
(datasetOption.isDefined()
101-
? datasetOption.get().acteeId
102-
: Actees.provision('dataset', project).then((actee) => (actee.id))))
103-
.then((acteeId) =>
104-
one(_createOrMerge(dataset, fields, acteeId, publish))
105-
.catch(error => {
106-
if (error.constraint === 'ds_property_fields_dspropertyid_formdefid_unique') {
107-
throw Problem.user.invalidEntityForm({ reason: 'Multiple Form Fields cannot be saved to a single Dataset Property.' });
108-
}
109-
throw error;
110-
}))
111-
.then((result) => Datasets.get(dataset.projectId, dataset.name).then((ds) => ds.get())
112-
.then((ds) => ((publish === true)
113-
? Datasets.getProperties(ds.id).then(properties => ({ ...ds, properties }))
114-
: ds.with({ action: result.action }))));
115-
116-
createOrMerge.audit = (dataset, _, fields, publish) => (log) =>
91+
// Takes the dataset name and field->property mappings defined in a form
92+
// and creates or updates the named dataset.
93+
// Arguments:
94+
// * dataset name
95+
// * form (a Form frame or object containing projectId, formDefId, and schemaId)
96+
// * array of field objects and property names that were parsed from the form xml
97+
// * publish flag saying whether or not to immediately publish the dataset
98+
const createOrMerge = (name, form, fields, publish) => async ({ one, Actees, Datasets, Projects }) => {
99+
const { projectId } = form;
100+
const { id: formDefId, schemaId } = form.def;
101+
102+
// Provision acteeId if necessary.
103+
// Need to check for existing dataset, and if not found, need to also
104+
// fetch the project since the dataset will be an actee child of the project.
105+
const existingDataset = await Datasets.get(projectId, name);
106+
const acteeId = existingDataset.isDefined()
107+
? existingDataset.get().acteeId
108+
: await Projects.getById(projectId).then((o) => o.get())
109+
.then((project) => Actees.provision('dataset', project))
110+
.then((actee) => (actee.id));
111+
const partial = new Dataset({ name, projectId, acteeId }, { formDefId });
112+
113+
// Prepare dataset properties from form fields:
114+
// Step 1: Filter to only fields with property name binds
115+
let dsPropertyFields = fields.filter((field) => (field.propertyName));
116+
// Step 2: Disallow properties to be named "name" or "label"
117+
if (dsPropertyFields.filter((field) => field.propertyName === 'name' || field.propertyName === 'label').length > 0)
118+
throw Problem.user.invalidEntityForm({ reason: 'Invalid Dataset property.' });
119+
// Step 3: Build Form Field frames to handle dataset property field insertion
120+
dsPropertyFields = dsPropertyFields.map((field) => new Form.Field(field, { propertyName: field.propertyName, schemaId, formDefId }));
121+
122+
// Insert or update: update dataset, dataset properties, and links to fields and current form
123+
// result contains action (created or updated) and the id of the dataset.
124+
const result = await one(_createOrMerge(partial, dsPropertyFields, acteeId, publish))
125+
.catch(error => {
126+
if (error.constraint === 'ds_property_fields_dspropertyid_formdefid_unique') {
127+
throw Problem.user.invalidEntityForm({ reason: 'Multiple Form Fields cannot be saved to a single Dataset Property.' });
128+
}
129+
throw error;
130+
});
131+
132+
// Fetch all published properties for this dataset, even beyond what is defined in this form.
133+
const publishedProperties = ((publish === true)
134+
? await Datasets.getProperties(result.id)
135+
: null);
136+
137+
// Partial contains acteeId which is needed for auditing.
138+
// Additional form fields and properties needed for audit logging as well.
139+
return partial.with({ action: result.action, fields: dsPropertyFields, properties: publishedProperties });
140+
};
141+
142+
createOrMerge.audit = (dataset, name, form, fields, publish) => (log) =>
117143
((dataset.action === 'created')
118-
? log('dataset.create', dataset, { fields: fields.map((f) => [f.path, f.propertyName]) })
119-
: log('dataset.update', dataset, { fields: fields.map((f) => [f.path, f.propertyName]) }))
144+
? log('dataset.create', dataset, { fields: dataset.fields.map((f) => [f.path, f.propertyName]) })
145+
: log('dataset.update', dataset, { fields: dataset.fields.map((f) => [f.path, f.propertyName]) }))
120146
.then(() => ((publish === true)
121147
? log('dataset.update.publish', dataset, { properties: dataset.properties.map(p => p.name) })
122148
: null));
123149
createOrMerge.audit.withResult = true;
124150

125-
126151
////////////////////////////////////////////////////////////////////////////
127152
// DATASET (AND DATASET PROPERTY) PUBLISH
128153

lib/model/query/forms.js

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const config = require('config');
1111
const { sql } = require('slonik');
1212
const { map } = require('ramda');
1313
const { Frame, into } = require('../frame');
14-
const { Actor, Blob, Form, Dataset } = require('../frames');
14+
const { Actor, Blob, Form } = require('../frames');
1515
const { getFormFields, merge, compare } = require('../../data/schema');
1616
const { getDataset } = require('../../data/dataset');
1717
const { generateToken } = require('../../util/crypto');
@@ -70,7 +70,7 @@ const createNew = (partial, project, publish = false) => async ({ run, Datasets,
7070
const keyId = await partial.aux.key.map(Keys.ensure).orElse(resolve(null));
7171

7272
// Parse form XML for fields and entity/dataset definitions
73-
const [fields, dataset] = await Promise.all([
73+
const [fields, datasetName] = await Promise.all([
7474
getFormFields(partial.xml),
7575
(partial.aux.key.isDefined() ? resolve(Option.none()) : getDataset(partial.xml)) // Don't parse dataset schema if Form has encryption key
7676
]);
@@ -95,15 +95,8 @@ const createNew = (partial, project, publish = false) => async ({ run, Datasets,
9595
]);
9696

9797
// Update datasets and properties if defined
98-
if (dataset.isDefined()) {
99-
const dsPropertyFields = fields.filter((field) => (field.propertyName));
100-
if (dsPropertyFields.filter((field) => field.propertyName === 'name' || field.propertyName === 'label').length > 0)
101-
throw Problem.user.invalidEntityForm({ reason: 'Invalid Dataset property.' });
102-
await Datasets.createOrMerge(
103-
new Dataset({ name: dataset.get(), projectId: project.id }, { formDefId: savedForm.def.id }),
104-
dsPropertyFields.map((field) => new Form.Field(field, { propertyName: field.propertyName, schemaId: savedForm.def.schemaId, formDefId: savedForm.def.id })),
105-
publish
106-
);
98+
if (datasetName.isDefined()) {
99+
await Datasets.createOrMerge(datasetName.get(), savedForm, fields, publish);
107100
}
108101

109102
// Return the final saved form
@@ -179,7 +172,7 @@ const createVersion = (partial, form, publish, duplicating = false) => async ({
179172
const keyId = await partial.aux.key.map(Keys.ensure).orElse(resolve(null));
180173

181174
// Parse form fields and dataset/entity definition from form XML
182-
const [fields, dataset] = await Promise.all([
175+
const [fields, datasetName] = await Promise.all([
183176
getFormFields(partial.xml),
184177
(partial.aux.key.isDefined() ? resolve(Option.none()) : getDataset(partial.xml))
185178
]);
@@ -236,13 +229,8 @@ const createVersion = (partial, form, publish, duplicating = false) => async ({
236229
]);
237230

238231
// Update datasets and properties if defined
239-
if (dataset.isDefined())
240-
await Datasets.createOrMerge(
241-
new Dataset({ name: dataset.get(), projectId: form.projectId }, { formDefId: savedDef.id }),
242-
fields.filter((field) => (field.propertyName)).map((field) => new Form.Field({ schemaId, ...field }, { propertyName: field.propertyName, formDefId: savedDef.id })),
243-
publish
244-
);
245-
232+
if (datasetName.isDefined())
233+
await Datasets.createOrMerge(datasetName.get(), new Form(form, { def: savedDef }), fieldsForInsert, publish);
246234
return savedDef;
247235
};
248236

0 commit comments

Comments
 (0)