Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import {
2 ContainerTypes,
3 ScalarTypes,
4 type Field,
5 type FieldType,
6} from '@roostorg/types';
7import _ from 'lodash';
8
9import { type NonEmptyArray } from '../../utils/typescript-types.js';
10import {
11 type ContentSchemaFieldRoles,
12 type ItemType,
13} from '../moderationConfigService/index.js';
14import { toNormalizedItemDataOrErrors } from './toNormalizedItemDataOrErrors.js';
15
16const { omit } = _;
17
18const fakeSchema = [
19 {
20 name: 'missingRequired',
21 type: ScalarTypes.BOOLEAN,
22 required: true,
23 container: null,
24 },
25 {
26 name: 'name',
27 type: ScalarTypes.STRING,
28 required: false,
29 container: null,
30 },
31 {
32 name: 'num',
33 type: ScalarTypes.NUMBER,
34 required: false,
35 container: null,
36 },
37 {
38 name: 'email',
39 required: true,
40 type: ScalarTypes.STRING,
41 container: null,
42 },
43 {
44 name: 'optionalNull',
45 type: ScalarTypes.STRING,
46 required: false,
47 container: null,
48 },
49 {
50 name: 'geohashInvalid',
51 type: ScalarTypes.GEOHASH,
52 required: false,
53 container: null,
54 },
55 {
56 name: 'requiredNull',
57 type: ScalarTypes.STRING,
58 required: true,
59 container: null,
60 },
61 {
62 name: 'requiredFalsey1',
63 type: ScalarTypes.BOOLEAN,
64 required: true,
65 container: null,
66 },
67 {
68 name: 'requiredFalsey2',
69 type: ScalarTypes.NUMBER,
70 required: true,
71 container: null,
72 },
73 {
74 name: 'requiredFalsey3',
75 type: ScalarTypes.STRING,
76 required: true,
77 container: null,
78 },
79 {
80 name: 'containerWithInvalidItem',
81 type: ContainerTypes.ARRAY,
82 required: true,
83 container: {
84 containerType: ContainerTypes.ARRAY,
85 valueScalarType: ScalarTypes.STRING,
86 keyScalarType: null,
87 },
88 },
89 {
90 name: 'validContainer',
91 type: ContainerTypes.ARRAY,
92 required: true,
93 container: {
94 containerType: ContainerTypes.ARRAY,
95 valueScalarType: ScalarTypes.STRING,
96 keyScalarType: null,
97 },
98 },
99 {
100 name: 'relatedItem',
101 type: ScalarTypes.RELATED_ITEM,
102 required: true,
103 container: null,
104 },
105 {
106 name: 'relatedItem2',
107 type: ScalarTypes.RELATED_ITEM,
108 required: true,
109 container: null,
110 },
111 {
112 name: 'relatedItem3',
113 type: ScalarTypes.RELATED_ITEM,
114 required: true,
115 container: null,
116 },
117] as const;
118
119const fakeUserSubmission = {
120 name: 'test',
121 // NB: we expect num to be valid, but not email.
122 num: '130', // should come out cast to a number in the snapshot.
123 email: 201,
124 optionalNull: null,
125 geohashInvalid: {
126 geometry: {
127 center: {
128 lat: 12,
129 lng: 12,
130 },
131 radius: 3,
132 },
133 },
134 // these should all be valid, until requiredNUll.
135 requiredFalsey1: false,
136 requiredFalsey2: 0,
137 requiredFalsey3: '',
138 requiredNull: null,
139 containerWithInvalidItem: ['test', null],
140 validContainer: ['test'],
141 relatedItem: {
142 id: 'test',
143 typeId: 'testTypeId',
144 name: 'test name',
145 },
146 relatedItem2: 'not a related item',
147 relatedItem3: {
148 id: 'test',
149 typeId: 'testTypeId',
150 name: { k: 'v' },
151 },
152};
153
154const fakeSchemaWithFieldRoles = [
155 {
156 name: 'createdAt',
157 type: ScalarTypes.STRING,
158 required: false,
159 container: null,
160 },
161 {
162 name: 'threadId',
163 type: ScalarTypes.RELATED_ITEM,
164 required: false,
165 container: null,
166 },
167 {
168 name: 'parentId',
169 type: ScalarTypes.RELATED_ITEM,
170 required: false,
171 container: null,
172 },
173] as const;
174const fakeFieldRoles = {
175 createdAt: 'createdAt',
176 threadId: 'threadId',
177 parentId: 'parentId',
178} as const;
179
180describe('Content type schemas', () => {
181 describe('toNormalizedItemDataOrErrors', () => {
182 test('should give proper errors', () => {
183 expect(
184 toNormalizedItemDataOrErrors(
185 ['testTypeId'],
186 getFakeItemTypeFromSchema(fakeSchema),
187 fakeUserSubmission,
188 ),
189 ).toMatchInlineSnapshot(`
190 [
191 [BadRequestError: Invalid Data for Item The field 'missingRequired' is required, but was not provided.],
192 [BadRequestError: Invalid Data for Item The field 'email' has an invalid value. The value you provided was: 201. This field, if given, must be a string.],
193 [BadRequestError: Invalid Data for Item The field 'geohashInvalid' has an invalid value. The value you provided was: {"geometry":{"center":{"lat":12,"lng":12},"radius":3}}. This field, if given, must be a valid geohash.],
194 [BadRequestError: Invalid Data for Item The field 'requiredNull' is required, but was not provided.],
195 [BadRequestError: Invalid Data for Item The field 'containerWithInvalidItem' has an invalid value. The value you provided was: ["test",null]. Some items in this field's array were not valid.],
196 [BadRequestError: Invalid Data for Item The field 'relatedItem2' has an invalid value. The value you provided was: "not a related item". This field, if given, must be an object with a (non-empty) string 'id' and a valid 'typeId', with an optional string name.],
197 [BadRequestError: Invalid Data for Item The field 'relatedItem3' has an invalid value. The value you provided was: {"id":"test","name":{"k":"v"},"typeId":"testTypeId"}. This field, if given, must be an object with a (non-empty) string 'id' and a valid 'typeId', with an optional string name.],
198 ]
199 `);
200 });
201
202 test('should return the normalized version of valid submissions', () => {
203 const schemaFieldsToRemove = new Set([
204 'missingRequired',
205 'requiredNull',
206 'containerWithInvalidItem',
207 'email',
208 'relatedItem2',
209 'relatedItem3',
210 ]);
211
212 const [validSubmission, validSubmissionSchema] = [
213 omit(fakeUserSubmission, [
214 'email',
215 'geohashInvalid',
216 'requiredNull',
217 'containerWithInvalidItem',
218 'relatedItem2',
219 'relatedItem3',
220 ]),
221 fakeSchema.filter(
222 (it) => !schemaFieldsToRemove.has(it.name),
223 ) as NonEmptyArray<(typeof fakeSchema)[number]>,
224 ];
225
226 expect(
227 toNormalizedItemDataOrErrors(
228 ['testTypeId'],
229 getFakeItemTypeFromSchema(validSubmissionSchema),
230 validSubmission,
231 ),
232 ).toMatchInlineSnapshot(`
233 {
234 "name": "test",
235 "num": 130,
236 "relatedItem": {
237 "id": "test",
238 "name": "test name",
239 "typeId": "testTypeId",
240 },
241 "requiredFalsey1": false,
242 "requiredFalsey2": 0,
243 "requiredFalsey3": "",
244 "validContainer": [
245 "test",
246 ],
247 }
248 `);
249 });
250 test('should fail for invalid field roles when only parentId is provided', () => {
251 expect(
252 toNormalizedItemDataOrErrors(
253 ['testTypeId'],
254 getFakeItemTypeFromSchema(fakeSchemaWithFieldRoles, fakeFieldRoles),
255 {
256 parentId: {
257 id: 'test',
258 typeId: 'testTypeId',
259 },
260 },
261 ),
262 ).toMatchInlineSnapshot(`
263 [
264 [BadRequestError: Invalid field roles for Item You provided us a parent: parentId without providing a value for when the item was created: createdAt or a value for the thread: threadId],
265 ]
266 `);
267 expect(
268 toNormalizedItemDataOrErrors(
269 ['testTypeId'],
270 getFakeItemTypeFromSchema(fakeSchemaWithFieldRoles, fakeFieldRoles),
271 {
272 threadId: {
273 id: 'test',
274 typeId: 'testTypeId',
275 },
276 },
277 ),
278 ).toMatchInlineSnapshot(`
279 [
280 [BadRequestError: Invalid field roles for Item You provided us a thread: threadId without providing a value for when the item was created: createdAt],
281 ]
282 `);
283 });
284 test('should pass for valid field roles', () => {
285 expect(
286 toNormalizedItemDataOrErrors(
287 ['testTypeId'],
288 getFakeItemTypeFromSchema(fakeSchemaWithFieldRoles, fakeFieldRoles),
289 {
290 threadId: {
291 id: 'test',
292 typeId: 'testTypeId',
293 },
294 parentId: {
295 id: 'test',
296 typeId: 'testTypeId',
297 },
298 createdAt: '2023-04-12T19:47:09.406Z',
299 },
300 ),
301 ).toMatchInlineSnapshot(`
302 {
303 "createdAt": "2023-04-12T19:47:09.406Z",
304 "parentId": {
305 "id": "test",
306 "typeId": "testTypeId",
307 },
308 "threadId": {
309 "id": "test",
310 "typeId": "testTypeId",
311 },
312 }
313 `);
314 });
315 });
316});
317
318function getFakeItemTypeFromSchema(
319 schema: readonly [Field<FieldType>, ...Field<FieldType>[]],
320 schemaFieldRoles: ContentSchemaFieldRoles = {},
321): ItemType {
322 return {
323 id: 'test',
324 kind: 'CONTENT',
325 name: 'test',
326 description: 'test',
327 version: 'test',
328 schemaVariant: 'original',
329 orgId: 'test orgId',
330 schemaFieldRoles,
331 schema,
332 };
333}