Skip to content

Commit d7060af

Browse files
authored
Merge pull request #12 from solocommand/custom-text-field
Custom text field support
2 parents a1fd266 + fd65698 commit d7060af

File tree

26 files changed

+673
-19
lines changed

26 files changed

+673
-19
lines changed

services/application/src/actions/field/create.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { handleError } = require('@identity-x/utils').mongoose;
44
const { Application } = require('../../mongodb/models');
55
const BooleanField = require('../../mongodb/models/field/boolean');
66
const SelectField = require('../../mongodb/models/field/select');
7+
const TextField = require('../../mongodb/models/field/text');
78
const prepareExternalId = require('./utils/prepare-external-id');
89

910
const createBoolean = async ({
@@ -56,12 +57,33 @@ const createSelect = async ({
5657
return select;
5758
};
5859

60+
const createText = async ({
61+
application,
62+
name,
63+
label,
64+
required,
65+
active,
66+
externalId: eid,
67+
} = {}) => {
68+
const externalId = prepareExternalId(eid);
69+
const text = new TextField({
70+
applicationId: application._id,
71+
name,
72+
label,
73+
required,
74+
active,
75+
externalId,
76+
});
77+
await text.save();
78+
return text;
79+
};
80+
5981
module.exports = async ({
6082
type,
6183
applicationId,
6284
payload = {},
6385
} = {}) => {
64-
const supportedTypes = ['select', 'boolean'];
86+
const supportedTypes = ['select', 'boolean', 'text'];
6587
if (!supportedTypes.includes(type)) throw createParamError('type', type, supportedTypes);
6688
if (!applicationId) throw createRequiredParamError('applicationId');
6789

@@ -73,8 +95,10 @@ module.exports = async ({
7395
switch (type) {
7496
case 'boolean':
7597
return createBoolean({ ...payload, application });
76-
default:
98+
case 'select':
7799
return createSelect({ ...payload, application });
100+
default:
101+
return createText({ ...payload, application });
78102
}
79103
} catch (e) {
80104
throw handleError(createError, e);

services/application/src/actions/field/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,31 @@ const create = require('./create');
88
const updateOne = require('./update-one');
99
const userBooleanAnswers = require('./user-boolean-answers');
1010
const userSelectAnswers = require('./user-select-answers');
11+
const userTextAnswers = require('./user-text-answers');
1112

1213
const Field = require('../../mongodb/models/field');
1314
const SelectField = require('../../mongodb/models/field/select');
1415
const BooleanField = require('../../mongodb/models/field/boolean');
16+
const TextField = require('../../mongodb/models/field/text');
1517

1618
module.exports = {
1719
create,
1820
findById: ({ id, type, fields }) => {
19-
const supportedTypes = ['select', 'boolean'];
21+
const supportedTypes = ['select', 'boolean', 'text'];
2022
if (!supportedTypes.includes(type)) throw createParamError('type', type, supportedTypes);
2123
switch (type) {
2224
case 'boolean':
2325
return findById(BooleanField, { id, fields });
24-
default:
26+
case 'select':
2527
return findById(SelectField, { id, fields });
28+
default:
29+
return findById(TextField, { id, fields });
2630
}
2731
},
2832
listForApp: params => listForApp(Field, params),
2933
matchForApp: params => matchForApp(Field, params),
3034
updateOne,
3135
userBooleanAnswers,
3236
userSelectAnswers,
37+
userTextAnswers,
3338
};

services/application/src/actions/field/update-one.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { handleError } = require('@identity-x/utils').mongoose;
55
const { Application } = require('../../mongodb/models');
66
const BooleanField = require('../../mongodb/models/field/boolean');
77
const SelectField = require('../../mongodb/models/field/select');
8+
const TextField = require('../../mongodb/models/field/text');
89
const prepareExternalId = require('./utils/prepare-external-id');
910

1011
const updateSelect = async ({
@@ -93,6 +94,35 @@ const updateBoolean = async ({
9394
await boolean.save();
9495
return boolean;
9596
};
97+
const updateText = async ({
98+
id,
99+
application,
100+
payload,
101+
} = {}) => {
102+
const text = await TextField.findByIdForApp(id, application._id);
103+
if (!text) throw createError(404, `No text field was found for '${id}'`);
104+
105+
const {
106+
name,
107+
label,
108+
required,
109+
active,
110+
externalId: eid,
111+
} = payload;
112+
113+
const externalId = prepareExternalId(eid);
114+
115+
text.set({
116+
name,
117+
label,
118+
required,
119+
active,
120+
externalId,
121+
});
122+
123+
await text.save();
124+
return text;
125+
};
96126

97127
module.exports = async ({
98128
id,
@@ -101,20 +131,21 @@ module.exports = async ({
101131
payload = {},
102132
} = {}) => {
103133
if (!id) throw createRequiredParamError('id');
104-
const supportedTypes = ['select', 'boolean'];
134+
const supportedTypes = ['select', 'boolean', 'text'];
105135
if (!supportedTypes.includes(type)) throw createParamError('type', type, supportedTypes);
106136
if (!applicationId) throw createRequiredParamError('applicationId');
107137

108138
const application = await Application.findById(applicationId, ['id']);
109139
if (!application) throw createError(404, `No application was found for '${applicationId}'`);
110140

111-
// for now, only select field types are supported.
112141
try {
113142
switch (type) {
114143
case 'boolean':
115144
return updateBoolean({ id, application, payload });
116-
default:
145+
case 'select':
117146
return updateSelect({ id, application, payload });
147+
default:
148+
return updateText({ id, application, payload });
118149
}
119150
} catch (e) {
120151
throw handleError(createError, e);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const { Sort } = require('@identity-x/pagination');
2+
const TextField = require('../../mongodb/models/field/text');
3+
4+
const { isArray } = Array;
5+
6+
module.exports = async ({
7+
applicationId,
8+
fieldIds,
9+
customTextFieldAnswers,
10+
onlyAnswered,
11+
onlyActive,
12+
sort,
13+
} = {}) => {
14+
const $sort = new Sort(sort);
15+
const fieldQuery = {
16+
applicationId,
17+
...(isArray(fieldIds) && fieldIds.length && { _id: { $in: fieldIds } }),
18+
...(onlyActive && { active: { $ne: false } }),
19+
};
20+
const fields = await TextField.find(fieldQuery, {}, { sort: $sort.value });
21+
// return nothing when no custom booleans are found.
22+
if (!fields.length) return [];
23+
24+
// ensure answers are an array
25+
const customFieldAnswers = !isArray(customTextFieldAnswers)
26+
|| !customTextFieldAnswers.length
27+
? []
28+
: customTextFieldAnswers;
29+
30+
31+
// return nothing when no answers are found and only answered questions have been requested.
32+
if (onlyAnswered && !customFieldAnswers.length) return [];
33+
34+
const mapped = fields.map((field) => {
35+
const fieldAnswer = customFieldAnswers.find(answer => `${answer._id}` === `${field._id}`);
36+
const value = fieldAnswer ? fieldAnswer.value : null;
37+
return {
38+
id: field._id,
39+
field,
40+
hasAnswered: Boolean(fieldAnswer),
41+
value,
42+
};
43+
});
44+
return onlyAnswered ? mapped.filter(field => field.hasAnswered) : mapped;
45+
};

services/application/src/actions/user/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const setUnverifiedData = require('./set-unverified-data');
2525
const updateCustomAttributes = require('./update-custom-attributes');
2626
const updateCustomBooleanAnswers = require('./update-custom-boolean-answers');
2727
const updateCustomSelectAnswers = require('./update-custom-select-answers');
28+
const updateCustomTextAnswers = require('./update-custom-text-answers');
2829
const updateOne = require('./update-one');
2930
const verifyAuth = require('./verify-auth');
3031

@@ -55,6 +56,7 @@ module.exports = {
5556
updateCustomAttributes,
5657
updateCustomBooleanAnswers,
5758
updateCustomSelectAnswers,
59+
updateCustomTextAnswers,
5860
updateOne,
5961
verifyAuth,
6062
setLastSeen: async ({ id }) => {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const { createError } = require('micro');
2+
const { createRequiredParamError } = require('@base-cms/micro').service;
3+
const { handleError } = require('@identity-x/utils').mongoose;
4+
5+
const { AppUser } = require('../../mongodb/models');
6+
7+
const { isArray } = Array;
8+
9+
module.exports = async ({
10+
id,
11+
applicationId,
12+
answers,
13+
profileLastVerifiedAt,
14+
} = {}) => {
15+
if (!id) throw createRequiredParamError('id');
16+
if (!applicationId) throw createRequiredParamError('applicationId');
17+
18+
const user = await AppUser.findByIdForApp(id, applicationId);
19+
if (!user) throw createError(404, `No user was found for '${id}'`);
20+
21+
// do not update user answers when passed answers are not an array
22+
if (!isArray(answers)) return user;
23+
24+
// get all current answers as object { id, value }
25+
const userObj = user.customTextFieldAnswers.reduce(
26+
(obj, item) => ({ ...obj, [item._id]: item.value }), {},
27+
);
28+
29+
// get new answers as object { id, value }
30+
const newAnswers = answers.reduce(
31+
(obj, item) => ({ ...obj, [item.fieldId]: item.value }), {},
32+
);
33+
34+
// merge new and old ansers to account for old non active answers
35+
const mergedAnswers = { ...userObj, ...newAnswers };
36+
37+
// convert merged answers into valid array of { _id, value } answers
38+
const toSet = Object.keys(mergedAnswers).map((key) => {
39+
const obj = { _id: key, value: mergedAnswers[key] };
40+
return obj;
41+
});
42+
43+
user.set('customTextFieldAnswers', toSet);
44+
if (profileLastVerifiedAt) {
45+
user.set('profileLastVerifiedAt', profileLastVerifiedAt);
46+
user.set('forceProfileReVerification', false);
47+
}
48+
try {
49+
await user.save();
50+
return user;
51+
} catch (e) {
52+
throw handleError(createError, e);
53+
}
54+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const Field = require('./index');
2+
const schema = require('../../schema/field/text');
3+
4+
module.exports = Field.discriminator('field-text', schema, 'text');

services/application/src/mongodb/models/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ const AppUserLogin = require('./app-user-login');
55
const Segment = require('./segment');
66
const Comment = require('./comment');
77
const CommentStream = require('./comment-stream');
8+
const FieldBoolean = require('./field/boolean');
89
const FieldSelect = require('./field/select');
10+
const FieldText = require('./field/text');
911
const Team = require('./team');
1012

1113
module.exports = {
@@ -16,6 +18,8 @@ module.exports = {
1618
Segment,
1719
Comment,
1820
CommentStream,
21+
FieldBoolean,
1922
FieldSelect,
23+
FieldText,
2024
Team,
2125
};

services/application/src/mongodb/schema/app-user.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ const customSelectFieldAnswerSchema = new Schema({
7272
},
7373
});
7474

75+
/**
76+
* The built-in `_id` field of this sub-document represents
77+
* the custom text field id.
78+
*/
79+
const customTextFieldAnswerSchema = new Schema({
80+
/**
81+
* The custom text field answer.
82+
*/
83+
value: {
84+
type: Schema.Types.String,
85+
trim: true,
86+
},
87+
});
88+
7589
const schema = new Schema({
7690
email: {
7791
type: String,
@@ -185,6 +199,10 @@ const schema = new Schema({
185199
type: [customSelectFieldAnswerSchema],
186200
default: () => [],
187201
},
202+
customTextFieldAnswers: {
203+
type: [customTextFieldAnswerSchema],
204+
default: () => [],
205+
},
188206
customAttributes: {
189207
type: Object,
190208
default: () => ({}),
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { Schema } = require('mongoose');
2+
3+
const schema = new Schema({});
4+
5+
module.exports = schema;

0 commit comments

Comments
 (0)