@becollective/utils 2.0.13 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,11 +4,17 @@ agent:
4
4
  machine:
5
5
  type: e1-standard-2
6
6
  os_image: ubuntu2404
7
+ global_job_config:
8
+ epilogue:
9
+ always:
10
+ commands:
11
+ - '[[ -f packages/utils/test-results/report.xml ]] && test-results publish packages/utils/test-results/report.xml || true'
7
12
  blocks:
8
13
  - name: Test
14
+ skip:
15
+ when: "change_in('/packages/utils', {default_branch: 'master', on_tags: false}) = false"
9
16
  task:
10
17
  secrets:
11
- - name: NPM
12
18
  - name: BUILD_DEPRECATED
13
19
  prologue:
14
20
  commands:
@@ -20,25 +26,10 @@ blocks:
20
26
  commands:
21
27
  - cd ./packages/utils
22
28
  - npm ci
23
- - npm run test
24
- - name: Publish
25
- skip:
26
- when: "branch != 'master'"
27
- task:
28
- secrets:
29
- - name: NPM
30
- - name: BUILD_DEPRECATED
31
- env_vars:
32
- - name: PACKAGE_DIR
33
- value: ./packages/utils
34
- prologue:
29
+ - npm run citest
30
+ after_pipeline:
31
+ task:
32
+ jobs:
33
+ - name: Merge reports
35
34
  commands:
36
- - checkout
37
- - export AWS_ACCESS_KEY_ID=$AWS_BUILD_DEPRECATED_ACCESS_KEY
38
- - export AWS_SECRET_ACCESS_KEY=$AWS_BUILD_DEPRECATED_SECRET_ACCESS_KEY
39
- jobs:
40
- - name: Publish
41
- commands:
42
- - echo "deploying @becollective/utils"
43
- - sh ./.semaphore/deployer.sh
44
-
35
+ - test-results gen-pipeline-report || echo "Report merge skipped (no artifacts or pull failed)"
package/jestconfig.json CHANGED
@@ -5,5 +5,16 @@
5
5
  "testRegex": "(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
6
6
  "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
7
7
  "collectCoverage": true,
8
- "setupFiles": ["./setup.ts"]
8
+ "setupFiles": ["./setup.ts"],
9
+ "reporters": [
10
+ "default",
11
+ [
12
+ "jest-junit",
13
+ {
14
+ "outputDirectory": "test-results",
15
+ "outputName": "report.xml",
16
+ "includeConsoleOutput": true
17
+ }
18
+ ]
19
+ ]
9
20
  }
package/lib/date-time.js CHANGED
@@ -70,7 +70,7 @@ const getShiftText = (from, to, timezone) => {
70
70
  return '';
71
71
  const startToTimezone = luxon_1.DateTime.fromJSDate(from).setZone(timezone);
72
72
  const endToTimezone = luxon_1.DateTime.fromJSDate(to).setZone(timezone);
73
- return `${startToTimezone.toFormat('cccc, DD, h:mma')}-${endToTimezone.toFormat('h:mma')}`;
73
+ return `${startToTimezone.toFormat('cccc, d MMMM yyyy, h:mma')}-${endToTimezone.toFormat('h:mma')}`;
74
74
  }
75
75
  catch (e) {
76
76
  return '';
package/lib/forms.d.ts CHANGED
@@ -46,6 +46,9 @@ export declare const validator: {
46
46
  'ui:bc-multiSelect': {
47
47
  type: string;
48
48
  };
49
+ 'ui:bc-allowCustomAnswer': {
50
+ type: string;
51
+ };
49
52
  'ui:widget': {
50
53
  type: string;
51
54
  enum: string[];
package/lib/forms.js CHANGED
@@ -312,6 +312,9 @@ exports.validator = {
312
312
  'ui:bc-multiSelect': {
313
313
  type: 'boolean',
314
314
  },
315
+ 'ui:bc-allowCustomAnswer': {
316
+ type: 'boolean',
317
+ },
315
318
  'ui:widget': {
316
319
  type: 'string',
317
320
  enum: widgetTypes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@becollective/utils",
3
- "version": "2.0.13",
3
+ "version": "2.0.14",
4
4
  "description": "Common utilities",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -11,17 +11,26 @@
11
11
  },
12
12
  "scripts": {
13
13
  "test": "npm run build && NODE_ENV=localtest jest --config jestconfig.json --detectOpenHandles --verbose --forceExit",
14
+ "citest": "npm run build && NODE_ENV=localtest jest --config jestconfig.json --detectOpenHandles --verbose --forceExit",
14
15
  "build": "tsc",
15
16
  "prepare": "npm run build"
16
17
  },
17
18
  "author": "",
18
19
  "license": "ISC",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/tickboxAU/becollective-npm.git"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
19
27
  "devDependencies": {
20
28
  "@types/jest": "^30.0.0",
21
29
  "@types/mongodb": "^4.0.7",
22
30
  "@types/node": "^25.6.0",
23
31
  "@types/pino": "^7.0.5",
24
32
  "jest": "^30.3.0",
33
+ "jest-junit": "^17.0.0",
25
34
  "prettier": "^3.8.3",
26
35
  "ts-jest": "^29.4.9",
27
36
  "tslint": "^6.1.3",
package/src/date-time.ts CHANGED
@@ -67,7 +67,7 @@ export const getShiftText = (from, to, timezone) => {
67
67
  if (!from || !to || !timezone) return '';
68
68
  const startToTimezone = DateTime.fromJSDate(from).setZone(timezone);
69
69
  const endToTimezone = DateTime.fromJSDate(to).setZone(timezone);
70
- return `${startToTimezone.toFormat('cccc, DD, h:mma')}-${endToTimezone.toFormat('h:mma')}`;
70
+ return `${startToTimezone.toFormat('cccc, d MMMM yyyy, h:mma')}-${endToTimezone.toFormat('h:mma')}`;
71
71
  }
72
72
  catch (e) {
73
73
  return '';
package/src/forms.ts CHANGED
@@ -359,6 +359,9 @@ export const validator = {
359
359
  'ui:bc-multiSelect': {
360
360
  type: 'boolean',
361
361
  },
362
+ 'ui:bc-allowCustomAnswer': {
363
+ type: 'boolean',
364
+ },
362
365
  'ui:widget': {
363
366
  type: 'string',
364
367
  enum: widgetTypes,
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- const validator = require('../src/forms');
2
+ const { validator } = require('../lib/forms');
3
3
  const testSchema = require('./forms.test-schema.json');
4
4
  const testUischema = require('./forms.test-uischema.json');
5
5
  const testData = require('./forms.test-data.json');
@@ -136,6 +136,40 @@ describe('validateSchema', () => {
136
136
  ]);
137
137
  });
138
138
 
139
+ test('should accept array schema with custom answer pattern in anyOf', () => {
140
+ const t = {
141
+ data: {
142
+ type: 'object',
143
+ required: [],
144
+ properties: {
145
+ q1: {
146
+ title: 'Pick one',
147
+ type: 'array',
148
+ uniqueItems: true,
149
+ items: {
150
+ type: 'string',
151
+ anyOf: [
152
+ { type: 'string', enum: ['Yes'] },
153
+ { type: 'string', enum: ['No'] },
154
+ { type: 'string', pattern: '^Other: .+' },
155
+ ],
156
+ },
157
+ },
158
+ },
159
+ },
160
+ ui: {
161
+ 'ui:order': ['q1'],
162
+ q1: {
163
+ 'ui:widget': 'WidgetRadio',
164
+ 'ui:title': 'Pick one',
165
+ 'ui:bc-allowCustomAnswer': true,
166
+ },
167
+ },
168
+ };
169
+
170
+ return expect(validateSchema(t.data, t.ui)).resolves.toBe(true);
171
+ });
172
+
139
173
  test('should succeed, returning true when schema and uischema are valid', () => {
140
174
  return expect(validateSchema(schema, uischema)).resolves.toBe(true);
141
175
  });
@@ -303,6 +337,58 @@ describe('validateData', () => {
303
337
 
304
338
  return expect(validateData(schema, data)).resolves.toBe(undefined);
305
339
  });
340
+
341
+ test('should accept custom answer values for choice fields', () => {
342
+ const customChoiceSchema = {
343
+ type: 'object',
344
+ required: [],
345
+ properties: {
346
+ q1: {
347
+ title: 'Pick one',
348
+ type: 'array',
349
+ uniqueItems: true,
350
+ items: {
351
+ type: 'string',
352
+ anyOf: [
353
+ { type: 'string', enum: ['Yes'] },
354
+ { type: 'string', enum: ['No'] },
355
+ { type: 'string', pattern: '^Other: .+' },
356
+ ],
357
+ },
358
+ },
359
+ },
360
+ };
361
+
362
+ return expect(
363
+ validateData(customChoiceSchema, { q1: ['Other: Prefer weekends'] }),
364
+ ).resolves.toBe(undefined);
365
+ });
366
+
367
+ test('should reject incomplete custom answer values for choice fields', () => {
368
+ const customChoiceSchema = {
369
+ type: 'object',
370
+ required: [],
371
+ properties: {
372
+ q1: {
373
+ title: 'Pick one',
374
+ type: 'array',
375
+ uniqueItems: true,
376
+ items: {
377
+ type: 'string',
378
+ anyOf: [
379
+ { type: 'string', enum: ['Yes'] },
380
+ { type: 'string', enum: ['No'] },
381
+ { type: 'string', pattern: '^Other: .+' },
382
+ ],
383
+ },
384
+ },
385
+ },
386
+ };
387
+
388
+ return expect(validateData(customChoiceSchema, { q1: ['Other'] })).rejects.toEqual([
389
+ expect.objectContaining({ name: 'Datatype is not an array of strings' }),
390
+ ]);
391
+ });
306
392
  });
307
393
 
308
394
  describe('stripEmptyArraysFromData', () => {
@@ -569,6 +655,40 @@ describe('dataTypeIsValid', () => {
569
655
  });
570
656
  });
571
657
 
658
+ describe('array with custom answer', () => {
659
+ const choice = {
660
+ schema: {
661
+ type: 'array',
662
+ title: 'Pick one',
663
+ items: {
664
+ type: 'string',
665
+ anyOf: [
666
+ { type: 'string', enum: ['Yes'] },
667
+ { type: 'string', enum: ['No'] },
668
+ { type: 'string', pattern: '^Other: .+' },
669
+ ],
670
+ },
671
+ uniqueItems: true,
672
+ },
673
+ };
674
+
675
+ test('should return true for a completed custom answer', () => {
676
+ expect(isValid.array(['Other: Prefer weekends'], choice.schema)).toBe(true);
677
+ });
678
+
679
+ test('should return true for a predefined option', () => {
680
+ expect(isValid.array(['No'], choice.schema)).toBe(true);
681
+ });
682
+
683
+ test('should return false for incomplete Other without text', () => {
684
+ expect(isValid.array(['Other'], choice.schema)).toBe(false);
685
+ });
686
+
687
+ test('should return false for arbitrary text not matching pattern', () => {
688
+ expect(isValid.array(['Maybe'], choice.schema)).toBe(false);
689
+ });
690
+ });
691
+
572
692
  describe('skills', () => {
573
693
  const skills = {
574
694
  schema: {
@@ -1 +0,0 @@
1
- export {};
@@ -1,11 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const parsing_1 = require("./parsing");
4
- it('removes null or undefined fields', () => {
5
- expect((0, parsing_1.toRruleStr)({
6
- DTSTART: '20120201T023000Z',
7
- DURATION: '1h',
8
- COUNT: null,
9
- UNTIL: undefined,
10
- })).toEqual('DTSTART=20120201T023000Z;DURATION=1h');
11
- });
@@ -1,12 +0,0 @@
1
- export declare class FeatureFlag {
2
- ttl: number;
3
- expiry: number;
4
- constructor(ttl?: any);
5
- isFeatureActive(feature: string, options: {
6
- env: string;
7
- region?: string;
8
- }): Promise<boolean>;
9
- clearCache(): void;
10
- setCacheTtl(ttl: number): void;
11
- }
12
- export default FeatureFlag;
@@ -1,51 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FeatureFlag = void 0;
4
- const cache = {
5
- store: new Map(),
6
- memoize: async (feature, options) => {
7
- const stringified = JSON.stringify({ feature, options });
8
- const response = cache.store.get(stringified);
9
- if (response) {
10
- return JSON.parse(response);
11
- }
12
- const fetchResponse = await fetchFeatureActive(feature, options);
13
- cache.store.set(stringified, JSON.stringify(fetchResponse));
14
- return fetchResponse;
15
- },
16
- };
17
- const fetchFeatureActive = async (feature, options) => {
18
- const getFlagByFeatureUrl = `https://lb-central.becollective.com/api/v2/feature-flags/${feature}`;
19
- let isFeatureActive = false;
20
- const featureFlags = await fetch(getFlagByFeatureUrl, { method: 'get' });
21
- const json = await featureFlags.json();
22
- const environments = json.Environments;
23
- if (environments) {
24
- isFeatureActive = environments.includes(options.env);
25
- }
26
- return isFeatureActive;
27
- };
28
- class FeatureFlag {
29
- constructor(ttl) {
30
- this.ttl = ttl || 1000 * 60 * 5; // default to 5 minutes
31
- this.expiry = Date.now() + ttl;
32
- }
33
- async isFeatureActive(feature, options) {
34
- // Lazy expiration upon function call
35
- const now = Date.now();
36
- if (now >= this.expiry) {
37
- cache.store.clear();
38
- this.expiry = now + this.ttl;
39
- }
40
- return cache.memoize(feature, options);
41
- }
42
- clearCache() {
43
- cache.store.clear();
44
- }
45
- setCacheTtl(ttl) {
46
- this.ttl = ttl;
47
- this.expiry = Date.now() + ttl;
48
- }
49
- }
50
- exports.FeatureFlag = FeatureFlag;
51
- exports.default = FeatureFlag;
@@ -1,28 +0,0 @@
1
- /**
2
- * Get current age based on the given birth date
3
- * @param {string} dateOfBirth
4
- * @param {string} unit the measurement of the the difference
5
- * The supported measurements are:
6
- * years, months, weeks, days, hours, minutes, and seconds.
7
- * @returns {number} age
8
- */
9
- export declare const getAge: (dateOfBirth: any, unit?: string) => number;
10
- /**
11
- * Check if a user requires parent(guardian) consent
12
- * i.e. Is under the age of 16 years, based on GDPR
13
- * https://gdpr-info.eu/art-8-gdpr/
14
- * @param {string} dateOfBirth moment.js compatible date time
15
- * @returns {boolean}
16
- * NOTE:
17
- * We used to not allow child (under 16) to register,
18
- * and birthdate is not a required field for ghost user
19
- * So an existing user could have no birthdate, we assume these users are adults
20
- * TODO:
21
- * Remove above logic after we migrated all legacy
22
- * data to have a consistent birthdate property.
23
- * Then we should throw error if birthdate is not valid
24
- */
25
- export declare const isUnderSixteen: (dateOfBirth: any) => boolean;
26
- export declare const isUnderAge: (dateOfBirth: any, limit: any, maxAcknowledgedAge?: number | null) => boolean;
27
- export declare const datesByThemselves: (a: any, b: any) => number;
28
- export declare const getShiftText: (from: any, to: any, timezone: any) => string;
@@ -1,79 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getShiftText = exports.datesByThemselves = exports.isUnderAge = exports.isUnderSixteen = exports.getAge = void 0;
4
- const luxon_1 = require("luxon");
5
- /**
6
- * Get current age based on the given birth date
7
- * @param {string} dateOfBirth
8
- * @param {string} unit the measurement of the the difference
9
- * The supported measurements are:
10
- * years, months, weeks, days, hours, minutes, and seconds.
11
- * @returns {number} age
12
- */
13
- const getAge = (dateOfBirth, unit = 'years') => {
14
- if (!dateOfBirth) {
15
- throw new Error('Invalid date');
16
- }
17
- const dob = luxon_1.DateTime.fromJSDate(new Date(dateOfBirth));
18
- if (!dob.isValid) {
19
- throw new Error('Invalid date');
20
- }
21
- const age = dob.diffNow(unit);
22
- const ageUnit = -age.as(unit);
23
- // Note: luxon returns scientific notation, ie 6.976154236428209e-9
24
- return Number(ageUnit.toString().split('.').shift());
25
- };
26
- exports.getAge = getAge;
27
- /**
28
- * Check if a user requires parent(guardian) consent
29
- * i.e. Is under the age of 16 years, based on GDPR
30
- * https://gdpr-info.eu/art-8-gdpr/
31
- * @param {string} dateOfBirth moment.js compatible date time
32
- * @returns {boolean}
33
- * NOTE:
34
- * We used to not allow child (under 16) to register,
35
- * and birthdate is not a required field for ghost user
36
- * So an existing user could have no birthdate, we assume these users are adults
37
- * TODO:
38
- * Remove above logic after we migrated all legacy
39
- * data to have a consistent birthdate property.
40
- * Then we should throw error if birthdate is not valid
41
- */
42
- const isUnderSixteen = (dateOfBirth) => {
43
- if (!dateOfBirth || !luxon_1.DateTime.fromJSDate(new Date(dateOfBirth)).isValid) {
44
- return false;
45
- }
46
- const age = (0, exports.getAge)(dateOfBirth);
47
- return isNaN(age) ? false : age < 16;
48
- };
49
- exports.isUnderSixteen = isUnderSixteen;
50
- const isUnderAge = (dateOfBirth, limit, maxAcknowledgedAge = null) => {
51
- if ((!dateOfBirth || !luxon_1.DateTime.fromJSDate(new Date(dateOfBirth)).isValid) && !maxAcknowledgedAge) {
52
- return false;
53
- }
54
- if (dateOfBirth) {
55
- const age = (0, exports.getAge)(dateOfBirth);
56
- return isNaN(age) ? true : age < limit;
57
- }
58
- return maxAcknowledgedAge ? maxAcknowledgedAge < limit : true;
59
- };
60
- exports.isUnderAge = isUnderAge;
61
- const datesByThemselves = (a, b) => {
62
- if (!a.from)
63
- return 1;
64
- return new Date(a.from).getTime() - new Date(b.from).getTime();
65
- };
66
- exports.datesByThemselves = datesByThemselves;
67
- const getShiftText = (from, to, timezone) => {
68
- try {
69
- if (!from || !to || !timezone)
70
- return '';
71
- const startToTimezone = luxon_1.DateTime.fromJSDate(from).setZone(timezone);
72
- const endToTimezone = luxon_1.DateTime.fromJSDate(to).setZone(timezone);
73
- return `${startToTimezone.toFormat('cccc, DD, h:mma')}-${endToTimezone.toFormat('h:mma')}`;
74
- }
75
- catch (e) {
76
- return '';
77
- }
78
- };
79
- exports.getShiftText = getShiftText;
@@ -1 +0,0 @@
1
- export {};