@laboratoria/sdk-js 0.2.0 → 1.3.0

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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ## Laboratoria JavaScript (browser) SDK
2
2
 
3
+ [![Node.js CI](https://github.com/Laboratoria/sdk-js/actions/workflows/node.js.yml/badge.svg)](https://github.com/Laboratoria/sdk-js/actions/workflows/node.js.yml)
4
+
3
5
  :warning: This tool is still in draft stage and is likely to change without
4
6
  notice.
5
7
 
@@ -25,7 +27,6 @@ const app = createApp({
25
27
  firebaseApiKey: '',
26
28
  firebaseProject: '',
27
29
  coreApiUrl: '',
28
- teamApiUrl: '',
29
30
  jobsApiUrl: '',
30
31
  });
31
32
  ```
@@ -79,7 +80,6 @@ app.user.create({
79
80
  ### Cohorts
80
81
 
81
82
  ```js
82
- // Cohorts API
83
83
  app.cohort.findMany({
84
84
  where: {
85
85
  end: { gt: new Date() },
@@ -96,13 +96,50 @@ app.cohort.findMany({
96
96
 
97
97
  ### Students
98
98
 
99
+ WIP
100
+
99
101
  ### Dropouts
100
102
 
103
+ WIP
101
104
 
102
105
  ### Projects
103
106
 
104
107
  ```js
105
108
  app.project.findMany();
106
109
 
107
- // app.learningObjectives.
110
+ app.project.findMany({ tag: 'v3.0.0' });
111
+
112
+ app.project.findMany({
113
+ locale: 'es-ES',
114
+ track: 'js',
115
+ });
116
+
117
+ app.project.findById('cipher');
118
+
119
+ app.project.findById('cipher', { tag: 'v3.0.0' });
108
120
  ```
121
+
122
+ ### Topics
123
+
124
+ ```js
125
+ app.topic.findMany();
126
+
127
+ app.topic.findMany({ tag: 'v3.0.0' });
128
+
129
+ app.topic.findMany({
130
+ locale: 'es-ES',
131
+ track: 'js',
132
+ });
133
+
134
+ app.topic.findById('javascript');
135
+
136
+ app.topic.findById('javascript', { tag: 'v3.0.0' });
137
+ ```
138
+
139
+ ### Learning Objectives
140
+
141
+ ```js
142
+ app.learningObjective.findMany();
143
+
144
+ app.learningObjective.findMany({ tag: 'v3.0.0' });
145
+ ```
package/index.js CHANGED
@@ -6,14 +6,13 @@ import {
6
6
  signOut,
7
7
  } from 'firebase/auth';
8
8
  import { createAPI as createCoreAPI } from './lib/core';
9
+ import { createAPI as createCurriculumAPI } from './lib/curriculum';
9
10
  import { createAPI as createJobsAPI } from './lib/jobs';
10
- import { createAPI as createTeamAPI } from './lib/team';
11
11
 
12
12
  export const createApp = ({
13
13
  firebaseApiKey = 'AIzaSyAXbaEbpq8NOfn0r8mIrcoHvoGRkJThwdc',
14
14
  firebaseProject = 'laboratoria-la',
15
15
  coreApiUrl = 'https://us-central1-outpost-272720.cloudfunctions.net/core-api',
16
- teamApiUrl = 'https://us-central1-outpost-272720.cloudfunctions.net/team-api',
17
16
  jobsApiUrl = 'https://us-central1-outpost-272720.cloudfunctions.net/jobs-api',
18
17
  } = {}) => {
19
18
  const firebaseApp = initializeApp({
@@ -26,8 +25,8 @@ export const createApp = ({
26
25
  const firebaseAuth = getAuth(firebaseApp);
27
26
  const state = { authUser: undefined, user: undefined };
28
27
  const coreAPI = createCoreAPI(coreApiUrl, state);
29
- const teamAPI = createTeamAPI(teamApiUrl, state);
30
28
  const jobsAPI = createJobsAPI(jobsApiUrl, state);
29
+ const curriculumAPI = createCurriculumAPI();
31
30
 
32
31
  const authAPI = {
33
32
  onChange: fn => onAuthStateChanged(firebaseAuth, (authUser) => {
@@ -56,9 +55,9 @@ export const createApp = ({
56
55
  };
57
56
 
58
57
  return {
59
- auth: authAPI,
60
58
  ...jobsAPI,
61
- ...teamAPI,
59
+ ...curriculumAPI,
62
60
  ...coreAPI,
61
+ auth: authAPI,
63
62
  };
64
63
  };
package/lib/core.js CHANGED
@@ -3,6 +3,11 @@ import { createModels, extendSchemaDefinitions } from './model';
3
3
  import schema from '../schemas/core.json';
4
4
 
5
5
  const extended = {
6
+ Country: {
7
+ primaryKey: 'code',
8
+ plural: 'countries',
9
+ searchProps: ['code', 'name'],
10
+ },
6
11
  User: {
7
12
  primaryKey: 'uid',
8
13
  inputProps: [
@@ -69,6 +74,7 @@ const extended = {
69
74
  properties: {},
70
75
  parse: (props) => {
71
76
  const now = Date.now();
77
+ // TODO: Handle case where start and/or end have not been selected in query
72
78
  const status = (
73
79
  props.start > now
74
80
  ? 'upcoming'
@@ -113,14 +119,33 @@ const extended = {
113
119
  'cohort.name',
114
120
  ],
115
121
  },
122
+ Contract: {
123
+ inputProps: [
124
+ 'user',
125
+ 'country',
126
+ 'isEmployment',
127
+ 'feeAmount',
128
+ 'hoursPerWeek',
129
+ 'start',
130
+ 'end',
131
+ ],
132
+ searchProps: ['user.firstName'],
133
+ getOptionLabel: ({ id, user }) => `${user?.firstName} (id: ${id})`,
134
+ },
135
+ Gig: {
136
+ inputProps: [
137
+ 'cohort',
138
+ 'user',
139
+ 'contract',
140
+ 'role',
141
+ 'hoursPerWeek',
142
+ 'start',
143
+ 'end',
144
+ ],
145
+ },
116
146
  };
117
147
 
118
148
  export const createAPI = (url, state) => createModels(url, state, {
119
149
  ...schema,
120
150
  definitions: extendSchemaDefinitions(schema, extended),
121
151
  });
122
-
123
- // const {
124
- // delete: _,
125
- // ...userAPI,
126
- // } = createModel(coreApiUrl, state, 'users', schemas.user);
@@ -0,0 +1,59 @@
1
+ const releasesUrl = 'https://api.github.com/repos/Laboratoria/bootcamp/releases';
2
+ const rawUrl = 'https://raw.githubusercontent.com/Laboratoria/bootcamp';
3
+
4
+ const getLatestVersion = () => fetch(`${releasesUrl}/latest`)
5
+ .then(resp => resp.json())
6
+ .then(({ tag_name }) => tag_name);
7
+
8
+ const fetchProjects = async tag => (
9
+ fetch(`${rawUrl}/${tag || await getLatestVersion()}/dist/projects.json`)
10
+ .then(resp => resp.json())
11
+ );
12
+
13
+ const fetchProject = async (slug, tag) => (
14
+ fetch(`${rawUrl}/${tag || await getLatestVersion()}/dist/projects/${slug}.json`)
15
+ .then(resp => resp.json())
16
+ );
17
+
18
+ const fetchTopics = async tag => (
19
+ fetch(`${rawUrl}/${tag || await getLatestVersion()}/dist/topics.json`)
20
+ .then(resp => resp.json())
21
+ );
22
+
23
+ const fetchTopic = async (slug, tag) => (
24
+ fetch(`${rawUrl}/${tag || await getLatestVersion()}/dist/topics/${slug}.json`)
25
+ .then(resp => resp.json())
26
+ );
27
+
28
+ const fetchLearningObjectives = async tag => (
29
+ fetch(`${rawUrl}/${tag || await getLatestVersion()}/dist/learning-objectives.json`)
30
+ .then(resp => resp.json())
31
+ );
32
+
33
+ const filterByLocaleAndTrack = opts => arr => arr.filter((obj) => {
34
+ if (opts.locale && opts.locale !== obj.locale) {
35
+ return false;
36
+ }
37
+ if (opts.track && opts.track !== obj.track) {
38
+ return false;
39
+ }
40
+ return true;
41
+ });
42
+
43
+ export const createAPI = () => {
44
+ return {
45
+ projects: {
46
+ findMany: (opts = {}) => fetchProjects(opts.tag)
47
+ .then(filterByLocaleAndTrack(opts)),
48
+ findById: (slug, opts = {}) => fetchProject(slug, opts.tag),
49
+ },
50
+ topics: {
51
+ findMany: (opts = {}) => fetchTopics(opts.tag)
52
+ .then(filterByLocaleAndTrack(opts)),
53
+ findById: (slug, opts = {}) => fetchTopic(slug, opts.tag),
54
+ },
55
+ learningObjectives: {
56
+ findMany: (opts = {}) => fetchLearningObjectives(opts.tag),
57
+ },
58
+ };
59
+ };
package/lib/model.js CHANGED
@@ -1,6 +1,38 @@
1
1
  import { createClient } from './client.js';
2
2
 
3
3
 
4
+ const isRequiredOneToOneRelation = (schema, key) => (
5
+ !!schema.properties
6
+ && !!schema.properties[key]
7
+ && !!schema.properties[key].$ref
8
+ );
9
+
10
+ const isOptionalOneToOneRelation = (schema, key) => (
11
+ !!schema.properties
12
+ && !!schema.properties[key]
13
+ && Array.isArray(schema.properties[key].anyOf)
14
+ && !!schema.properties[key].anyOf[0]?.$ref
15
+ && schema.properties[key].anyOf[1]?.type === 'null'
16
+ );
17
+
18
+ const isOneToOneRelation = (schema, key) => (
19
+ isRequiredOneToOneRelation(schema, key)
20
+ || isOptionalOneToOneRelation(schema, key)
21
+ );
22
+
23
+ const isOneToManyRelation = (schema, key) => (
24
+ schema.properties
25
+ && schema.properties[key]
26
+ && schema.properties[key].type === 'array'
27
+ && !!schema.properties[key].items.$ref
28
+ );
29
+
30
+ // const isRelation = (schema, key) => (
31
+ // isOneToOneRelation(schema, key)
32
+ // || isOneToManyRelation(schema, key)
33
+ // );
34
+
35
+
4
36
  const createValidator = (schema) => {
5
37
  const properties = schema.properties || {};
6
38
  const inputProps = schema.inputProps || Object.keys(properties);
@@ -71,44 +103,14 @@ const createBuildURL = collectionName => (id, q) => {
71
103
  };
72
104
 
73
105
 
74
- const createBuildQuery = defaultInclude => (q = {}) => ({
75
- ...(!q.select && Object.keys(defaultInclude).length && { include: defaultInclude }),
76
- ...q,
77
- });
78
-
79
-
80
- const buildDefaultInclude = schema => Object.keys(schema.properties || {}).reduce(
81
- (memo, key) => (
82
- (
83
- schema.properties[key].$ref
84
- || (
85
- Array.isArray(schema.properties[key].anyOf)
86
- && schema.properties[key].anyOf[0].$ref
87
- )
88
- )
89
- ? { ...memo, [key]: true }
90
- : memo
91
- ),
92
- {},
93
- );
94
-
95
-
96
106
  const serializeData = (data, schema) => {
97
107
  const hasInputProps = Array.isArray(schema.inputProps);
98
- const payload = Object.keys(data).reduce(
108
+ return Object.keys(data).reduce(
99
109
  (memo, key) => {
100
110
  if (hasInputProps && !schema.inputProps.includes(key)) {
101
111
  return memo;
102
112
  }
103
- const prop = (schema.properties || {})[key];
104
- const isOptionalRelation = (
105
- prop
106
- && Array.isArray(prop.anyOf)
107
- && prop.anyOf[0].$ref
108
- && prop.anyOf[1]
109
- && prop.anyOf[1].type === 'null'
110
- );
111
- if (isOptionalRelation && data[key] === null) {
113
+ if (isOptionalOneToOneRelation(schema, key) && data[key] === null) {
112
114
  return memo;
113
115
  }
114
116
  return {
@@ -118,14 +120,6 @@ const serializeData = (data, schema) => {
118
120
  },
119
121
  {},
120
122
  );
121
-
122
- // {
123
- // signupCohort: {
124
- // connect: { id: signupCohort.id },
125
- // },
126
- // }
127
-
128
- return payload;
129
123
  };
130
124
 
131
125
 
@@ -133,18 +127,18 @@ export const createModel = (baseUrl, state, collectionName, schema = {}) => {
133
127
  const primaryKey = schema.primaryKey || 'id';
134
128
  const validator = createValidator(schema);
135
129
  const buildURL = createBuildURL(collectionName);
136
- const defaultInclude = buildDefaultInclude(schema);
137
- const buildQuery = createBuildQuery(defaultInclude);
138
130
  const req = (...args) => createClient(baseUrl, state.authUser)(...args);
139
- const create = q => req(buildURL(), {
131
+ const create = ({ data, ...q }) => req(buildURL(null, q), {
140
132
  method: 'POST',
141
- body: buildQuery(Object.assign(q, { data: serializeData(q.data, schema) })),
142
- });
143
-
144
- const put = q => req(buildURL(q.where[primaryKey]), {
145
- method: 'PUT',
146
- body: buildQuery(Object.assign(q, { data: serializeData(q.data, schema) })),
133
+ body: serializeData(data, schema),
147
134
  });
135
+ const put = ({ data, ...q }) => {
136
+ const { where, ...rest } = q;
137
+ return req(buildURL(where[primaryKey], rest), {
138
+ method: 'PUT',
139
+ body: serializeData(data, schema),
140
+ });
141
+ };
148
142
 
149
143
  const parse = data => {
150
144
  const parsed = Object.keys(data).reduce(
@@ -168,18 +162,48 @@ export const createModel = (baseUrl, state, collectionName, schema = {}) => {
168
162
  );
169
163
  };
170
164
 
165
+ const relations = Object.keys(schema.properties || {}).reduce(
166
+ (memo, key) => (
167
+ isRequiredOneToOneRelation(schema, key)
168
+ ? Object.assign(memo, {
169
+ all: memo.all.concat(key),
170
+ oneToOne: memo.oneToOne.concat(key),
171
+ requiredOneToOne: memo.requiredOneToOne.concat(key),
172
+ })
173
+ : isOptionalOneToOneRelation(schema, key)
174
+ ? Object.assign(memo, {
175
+ all: memo.all.concat(key),
176
+ oneToOne: memo.oneToOne.concat(key),
177
+ optionalOneToOne: memo.optionalOneToOne.concat(key),
178
+ })
179
+ : isOneToManyRelation(schema, key)
180
+ ? Object.assign(memo, {
181
+ all: memo.all.concat(key),
182
+ oneToMany: memo.oneToMany.concat(key),
183
+ })
184
+ : memo
185
+ ),
186
+ {
187
+ all: [],
188
+ oneToOne: [],
189
+ requiredOneToOne: [],
190
+ optionalOneToOne: [],
191
+ oneToMany: [],
192
+ },
193
+ );
194
+
171
195
  return {
172
196
  get schema() {
173
- return schema;
197
+ return { ...schema, primaryKey };
174
198
  },
175
- get include() {
176
- return defaultInclude;
199
+ get relations() {
200
+ return relations;
177
201
  },
178
202
  validateAttr: validator.validateAttr,
179
203
  validate: validator.validate,
180
- findMany: q => req(buildURL(null, buildQuery(q)))
204
+ findMany: q => req(buildURL(null, q))
181
205
  .then(results => results.map(parse)),
182
- findById: (id, q) => req(buildURL(id, buildQuery(q)))
206
+ findById: (id, q) => req(buildURL(id, q))
183
207
  .then(raw => !!raw ? parse(raw) : raw),
184
208
  create,
185
209
  update: put,
package/package.json CHANGED
@@ -1,24 +1,33 @@
1
1
  {
2
2
  "name": "@laboratoria/sdk-js",
3
- "version": "0.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Laboratoria JavaScript (browser) SDK",
5
5
  "scripts": {
6
- "test": "jest --verbose --coverage"
6
+ "test": "jest --verbose --coverage",
7
+ "changelog": "git log $(git describe --tags --abbrev=0)..HEAD --oneline --format=\"* %h %s (%an)\""
7
8
  },
8
9
  "license": "MIT",
9
10
  "dependencies": {
10
11
  "blueimp-md5": "^2.19.0",
11
- "firebase": "^9.6.2"
12
+ "firebase": "^9.6.6"
12
13
  },
13
14
  "devDependencies": {
14
- "@babel/core": "^7.16.7",
15
+ "@babel/core": "^7.17.2",
15
16
  "@babel/plugin-transform-modules-commonjs": "^7.16.8",
16
- "babel-jest": "^27.4.6",
17
- "jest": "^27.4.7",
18
- "webpack": "^5.65.0",
19
- "webpack-cli": "^4.9.1"
17
+ "babel-jest": "^27.5.1",
18
+ "jest": "^27.5.1",
19
+ "webpack": "^5.68.0",
20
+ "webpack-cli": "^4.9.2"
20
21
  },
21
22
  "jest": {
22
- "testEnvironment": "jsdom"
23
+ "testEnvironment": "jsdom",
24
+ "coverageThreshold": {
25
+ "global": {
26
+ "statements": 97,
27
+ "branches": 92,
28
+ "functions": 98,
29
+ "lines": 97
30
+ }
31
+ }
23
32
  }
24
33
  }
package/schemas/core.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "definitions": {
4
+ "Country": {
5
+ "type": "object",
6
+ "properties": {
7
+ "code": {
8
+ "type": "string"
9
+ },
10
+ "name": {
11
+ "type": "string"
12
+ },
13
+ "contracts": {
14
+ "type": "array",
15
+ "items": {
16
+ "$ref": "#/definitions/Contract"
17
+ }
18
+ }
19
+ }
20
+ },
4
21
  "User": {
5
22
  "type": "object",
6
23
  "properties": {
@@ -99,6 +116,18 @@
99
116
  "type": "null"
100
117
  }
101
118
  ]
119
+ },
120
+ "contracts": {
121
+ "type": "array",
122
+ "items": {
123
+ "$ref": "#/definitions/Contract"
124
+ }
125
+ },
126
+ "gigs": {
127
+ "type": "array",
128
+ "items": {
129
+ "$ref": "#/definitions/Gig"
130
+ }
102
131
  }
103
132
  }
104
133
  },
@@ -270,20 +299,26 @@
270
299
  "$ref": "#/definitions/Dropout"
271
300
  }
272
301
  },
302
+ "gigs": {
303
+ "type": "array",
304
+ "items": {
305
+ "$ref": "#/definitions/Gig"
306
+ }
307
+ },
273
308
  "links": {
274
309
  "type": "array",
275
310
  "items": {
276
311
  "$ref": "#/definitions/CohortLink"
277
312
  }
278
313
  },
279
- "legacySlug": {
280
- "type": "string"
281
- },
282
314
  "signupUsers": {
283
315
  "type": "array",
284
316
  "items": {
285
317
  "$ref": "#/definitions/User"
286
318
  }
319
+ },
320
+ "legacySlug": {
321
+ "type": "string"
287
322
  }
288
323
  }
289
324
  },
@@ -307,7 +342,7 @@
307
342
  "null"
308
343
  ]
309
344
  },
310
- "studentCode": {
345
+ "legacyStudentCode": {
311
346
  "type": [
312
347
  "string",
313
348
  "null"
@@ -415,10 +450,115 @@
415
450
  "$ref": "#/definitions/User"
416
451
  }
417
452
  }
453
+ },
454
+ "Contract": {
455
+ "type": "object",
456
+ "properties": {
457
+ "id": {
458
+ "type": "integer"
459
+ },
460
+ "createdAt": {
461
+ "type": "string",
462
+ "format": "date-time"
463
+ },
464
+ "updatedAt": {
465
+ "type": "string",
466
+ "format": "date-time"
467
+ },
468
+ "createdBy": {
469
+ "type": "string"
470
+ },
471
+ "start": {
472
+ "type": "string",
473
+ "format": "date-time"
474
+ },
475
+ "end": {
476
+ "type": [
477
+ "string",
478
+ "null"
479
+ ],
480
+ "format": "date-time"
481
+ },
482
+ "hoursPerWeek": {
483
+ "type": "integer"
484
+ },
485
+ "isEmployment": {
486
+ "type": "boolean"
487
+ },
488
+ "feeAmount": {
489
+ "type": "integer"
490
+ },
491
+ "user": {
492
+ "$ref": "#/definitions/User"
493
+ },
494
+ "country": {
495
+ "$ref": "#/definitions/Country"
496
+ },
497
+ "gigs": {
498
+ "type": "array",
499
+ "items": {
500
+ "$ref": "#/definitions/Gig"
501
+ }
502
+ }
503
+ }
504
+ },
505
+ "Gig": {
506
+ "type": "object",
507
+ "properties": {
508
+ "id": {
509
+ "type": "integer"
510
+ },
511
+ "createdAt": {
512
+ "type": "string",
513
+ "format": "date-time"
514
+ },
515
+ "updatedAt": {
516
+ "type": "string",
517
+ "format": "date-time"
518
+ },
519
+ "createdBy": {
520
+ "type": "string"
521
+ },
522
+ "start": {
523
+ "type": "string",
524
+ "format": "date-time"
525
+ },
526
+ "end": {
527
+ "type": [
528
+ "string",
529
+ "null"
530
+ ],
531
+ "format": "date-time"
532
+ },
533
+ "hoursPerWeek": {
534
+ "type": "integer"
535
+ },
536
+ "role": {
537
+ "type": "string",
538
+ "enum": [
539
+ "bm",
540
+ "pdc",
541
+ "js",
542
+ "ux"
543
+ ]
544
+ },
545
+ "user": {
546
+ "$ref": "#/definitions/User"
547
+ },
548
+ "contract": {
549
+ "$ref": "#/definitions/Contract"
550
+ },
551
+ "cohort": {
552
+ "$ref": "#/definitions/Cohort"
553
+ }
554
+ }
418
555
  }
419
556
  },
420
557
  "type": "object",
421
558
  "properties": {
559
+ "country": {
560
+ "$ref": "#/definitions/Country"
561
+ },
422
562
  "user": {
423
563
  "$ref": "#/definitions/User"
424
564
  },
@@ -442,6 +582,12 @@
442
582
  },
443
583
  "dropout": {
444
584
  "$ref": "#/definitions/Dropout"
585
+ },
586
+ "contract": {
587
+ "$ref": "#/definitions/Contract"
588
+ },
589
+ "gig": {
590
+ "$ref": "#/definitions/Gig"
445
591
  }
446
592
  }
447
593
  }
@@ -1,31 +0,0 @@
1
- # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
- # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
-
4
- name: Node.js CI
5
-
6
- on:
7
- push:
8
- branches: [ main ]
9
- pull_request:
10
- branches: [ main ]
11
-
12
- jobs:
13
- build:
14
-
15
- runs-on: ubuntu-latest
16
-
17
- strategy:
18
- matrix:
19
- node-version: [14.x, 16.x]
20
- # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21
-
22
- steps:
23
- - uses: actions/checkout@v2
24
- - name: Use Node.js ${{ matrix.node-version }}
25
- uses: actions/setup-node@v2
26
- with:
27
- node-version: ${{ matrix.node-version }}
28
- cache: 'npm'
29
- - run: npm ci
30
- - run: npm run build --if-present
31
- - run: npm test
package/index.spec.js DELETED
@@ -1,112 +0,0 @@
1
- import { initializeApp } from 'firebase/app';
2
- import {
3
- getAuth,
4
- onAuthStateChanged,
5
- signInWithEmailAndPassword,
6
- signOut,
7
- } from 'firebase/auth';
8
- import { createClient } from './lib/client.js';
9
- import { createApp } from './index.js';
10
-
11
- jest.mock('./lib/client.js');
12
-
13
- beforeEach(() => {
14
- initializeApp.mockClear();
15
- onAuthStateChanged.mockClear();
16
- signInWithEmailAndPassword.mockClear();
17
- });
18
-
19
- describe('createApp', () => {
20
- it('should invoke firebase\'s initializaApp', () => {
21
- createApp();
22
- expect(initializeApp).toHaveBeenCalledTimes(1);
23
- expect(initializeApp.mock.calls[0]).toMatchSnapshot();
24
- });
25
- });
26
-
27
- describe('app.auth.onChange', () => {
28
- it('should listen to firebase\'s onAuthStateChanged and notify subscribers when not authenticated', (done) => {
29
- const { auth } = createApp();
30
-
31
- onAuthStateChanged.mockImplementationOnce((_, cb) => {
32
- cb();
33
- return () => { };
34
- });
35
-
36
- auth.onChange(({ authUser, user }) => {
37
- expect(authUser).toBeNull();
38
- expect(user).toBeNull();
39
- done();
40
- });
41
- });
42
-
43
- it('should fetch user from db when authenticated and add isStaff, isManager, etc', (done) => {
44
- const { auth } = createApp();
45
- const mockAuthUser = { uid: 'xxx', getIdToken: () => 'token' };
46
- const userMock = { uid: 'xxx', email: 'foo@bar.baz' };
47
-
48
- onAuthStateChanged.mockImplementationOnce((_, cb) => {
49
- cb(mockAuthUser);
50
- return () => { };
51
- });
52
-
53
- createClient().mockResolvedValueOnce(userMock);
54
-
55
- auth.onChange(({ authUser, user }) => {
56
- expect(authUser).toEqual(mockAuthUser);
57
- expect(user).toEqual({
58
- ...userMock,
59
- isStaff: false,
60
- isManager: false,
61
- isFinance: false,
62
- isAdmin: false,
63
- });
64
- done();
65
- });
66
- });
67
-
68
- it('should log error and reset state (authUser = null, user = null) when fetch user from db fails', (done) => {
69
- const { auth } = createApp();
70
- const mockAuthUser = { uid: 'xxx', getIdToken: () => 'token' };
71
- const spy = jest.spyOn(console, 'error').mockImplementationOnce(() => { })
72
- const error = new Error('OMG');
73
-
74
- onAuthStateChanged.mockImplementationOnce((_, cb) => {
75
- cb(mockAuthUser);
76
- return () => { };
77
- });
78
-
79
- createClient().mockRejectedValueOnce(error);
80
-
81
- auth.onChange(({ authUser, user }) => {
82
- expect(authUser).toBeNull();
83
- expect(user).toBeNull();
84
- expect(spy).toHaveBeenCalledWith(error);
85
- spy.mockRestore();
86
- done();
87
- });
88
- });
89
- });
90
-
91
- describe('app.auth.signIn', () => {
92
- it('should delegate to firebase\'s signInWithEmailAndPassword', async () => {
93
- const { auth } = createApp();
94
- const email = 'foo@bar.baz';
95
- const pass = 'secret';
96
-
97
- await auth.signIn(email, pass);
98
-
99
- expect(signInWithEmailAndPassword).toHaveBeenCalledTimes(1);
100
- expect(signInWithEmailAndPassword).toHaveBeenCalledWith({}, email, pass);
101
- });
102
- });
103
-
104
- describe('app.auth.signOut', () => {
105
- it('should delegate to firebase\'s signOut', async () => {
106
- const { auth } = createApp();
107
-
108
- await auth.signOut();
109
-
110
- expect(signOut).toHaveBeenCalledTimes(1);
111
- });
112
- });
@@ -1,77 +0,0 @@
1
- import { createClient } from './client.js';
2
-
3
- describe('client', () => {
4
-
5
- beforeAll(() => window.fetch = jest.fn());
6
-
7
- afterEach(() => {
8
- window.fetch.mockClear();
9
- jest.restoreAllMocks();
10
- });
11
-
12
- it('should delegate to global fetch and add mode:cors and parse JSON resp when OK', async () => {
13
- window.fetch.mockResolvedValue({
14
- json: jest.fn().mockResolvedValue({ ok: true }),
15
- });
16
- const baseUrl = 'http://foo.bar';
17
- const client = createClient(baseUrl);
18
- expect(await client('/')).toEqual({ ok: true });
19
- expect(window.fetch).toHaveBeenCalledTimes(1);
20
- expect(window.fetch).toHaveBeenCalledWith(`${baseUrl}/`, {
21
- mode: 'cors',
22
- headers: {},
23
- });
24
- });
25
-
26
- it('should reject when response.status > 202', async () => {
27
- window.fetch.mockResolvedValue({
28
- status: 400,
29
- json: jest.fn().mockResolvedValue({ ok: false }),
30
- });
31
- const baseUrl = 'http://foo.bar';
32
- const client = createClient(baseUrl);
33
- await expect(client('/')).rejects.toThrow('HTTP Error 400');
34
- expect(window.fetch).toHaveBeenCalledTimes(1);
35
- expect(window.fetch).toHaveBeenCalledWith(`${baseUrl}/`, {
36
- mode: 'cors',
37
- headers: {},
38
- });
39
- });
40
-
41
- it('should add token when authUser present', async () => {
42
- window.fetch.mockResolvedValue({
43
- json: jest.fn().mockResolvedValue({ ok: true }),
44
- });
45
- const baseUrl = 'http://foo.bar';
46
- const user = { getIdToken: jest.fn().mockReturnValueOnce('xxx') };
47
- const client = createClient(baseUrl, user);
48
- expect(await client('/')).toEqual({ ok: true });
49
- expect(window.fetch).toHaveBeenCalledTimes(1);
50
- expect(window.fetch).toHaveBeenCalledWith(`${baseUrl}/`, {
51
- mode: 'cors',
52
- headers: {
53
- authorization: 'Bearer xxx',
54
- },
55
- });
56
- expect(user.getIdToken).toHaveBeenCalledTimes(1);
57
- });
58
-
59
- it('should add content-type header and stringify body when necessary', async () => {
60
- window.fetch.mockResolvedValue({
61
- json: jest.fn().mockResolvedValue({ ok: true }),
62
- });
63
- const baseUrl = 'http://foo.bar';
64
- const client = createClient(baseUrl);
65
- const body = { foo: 'bar' };
66
- expect(await client('/', { method: 'POST', body })).toEqual({ ok: true });
67
- expect(window.fetch).toHaveBeenCalledTimes(1);
68
- expect(window.fetch).toHaveBeenCalledWith(`${baseUrl}/`, {
69
- mode: 'cors',
70
- headers: {
71
- 'content-type': 'application/json',
72
- },
73
- method: 'POST',
74
- body: JSON.stringify(body),
75
- });
76
- });
77
- });
package/lib/currencies.js DELETED
@@ -1,3 +0,0 @@
1
- const currencies = ['BRL', 'CLP', 'COP', 'MXN', 'PEN', 'USD'];
2
-
3
- export default currencies;
package/lib/model.spec.js DELETED
@@ -1,243 +0,0 @@
1
- import { createModel } from './model.js';
2
- import { createClient } from './client.js';
3
-
4
- jest.mock('./client.js');
5
-
6
- beforeEach(() => createClient().mockClear());
7
-
8
- describe('model.schema', () => {
9
- it('should be empty object by default', () => {
10
- const model = createModel('http://1.2.3.4', {}, 'foo');
11
- expect(model.schema).toEqual({});
12
- });
13
-
14
- it('should get the model\'s schema', () => {
15
- const schema = {
16
- properties: {
17
- a: { type: 'string' },
18
- },
19
- };
20
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
21
- expect(model.schema).toEqual(schema);
22
- });
23
- });
24
-
25
- describe('model.findMany(options)', () => {
26
- it('should send GET request', async () => {
27
- const schema = {
28
- properties: {
29
- id: { type: 'integer' },
30
- createdAt: { type: 'string', format: 'date-time' },
31
- },
32
- };
33
- const now = new Date();
34
- const client = createClient().mockResolvedValue([{
35
- id: 1,
36
- createdAt: now.toISOString(),
37
- }]);
38
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
39
- const results = await model.findMany();
40
- expect(results).toEqual([{ id: 1, createdAt: now }]);
41
- expect(results[0].createdAt instanceof Date).toBe(true);
42
- expect(client).toHaveBeenCalledTimes(1);
43
- expect(client).toHaveBeenCalledWith('/foo');
44
- });
45
- });
46
-
47
- describe('model.findById(id, options)', () => {
48
- it('should send GET request', async () => {
49
- const client = createClient().mockResolvedValue({});
50
- const model = createModel('http://1.2.3.4', {}, 'foo');
51
- expect(await model.findById(1)).toEqual({});
52
- expect(client).toHaveBeenCalledTimes(1);
53
- expect(client).toHaveBeenCalledWith('/foo/1');
54
- });
55
- });
56
-
57
- describe('model.create(options)', () => {
58
- it('should send POST request', async () => {
59
- const client = createClient().mockResolvedValue({});
60
- const model = createModel('http://1.2.3.4', {}, 'foo');
61
- expect(await model.create({ data: { ok: true } })).toEqual({});
62
- expect(client).toHaveBeenCalledTimes(1);
63
- expect(client).toHaveBeenCalledWith('/foo', {
64
- body: { data: { ok: true } },
65
- method: 'POST',
66
- });
67
- });
68
- });
69
-
70
- describe('model.update(options)', () => {
71
- it('should send POST request', async () => {
72
- const client = createClient().mockResolvedValue({});
73
- const model = createModel('http://1.2.3.4', {}, 'foo');
74
- expect(await model.update({ where: { id: 1 }, data: { ok: true } })).toEqual({});
75
- expect(client).toHaveBeenCalledTimes(1);
76
- expect(client).toHaveBeenCalledWith('/foo/1', {
77
- body: { where: { id: 1 }, data: { ok: true } },
78
- method: 'PUT',
79
- });
80
- });
81
-
82
- it('should exclude null values for optional relation props', async () => {
83
- const client = createClient().mockResolvedValue({});
84
- const schema = {
85
- properties: {
86
- signupCohort: {
87
- anyOf: [
88
- { '$ref': '#/definitions/Cohort' },
89
- { type: 'null' },
90
- ],
91
- },
92
- },
93
- };
94
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
95
- expect(await model.update({
96
- where: { id: 1 },
97
- data: { signupCohort: null },
98
- })).toEqual({});
99
- expect(client).toHaveBeenCalledTimes(1);
100
- expect(client).toHaveBeenCalledWith('/foo/1', {
101
- body: {
102
- where: { id: 1 },
103
- data: {},
104
- include: { signupCohort: true },
105
- },
106
- method: 'PUT',
107
- });
108
- });
109
- });
110
-
111
- describe('model.upsert(options)', () => {
112
- it('should send POST request when new row', async () => {
113
- const client = createClient().mockResolvedValue({});
114
- const model = createModel('http://1.2.3.4', {}, 'foo');
115
- const result = await model.upsert({
116
- where: {},
117
- create: { ok: true },
118
- });
119
- expect(result).toEqual({});
120
- expect(client).toHaveBeenCalledTimes(1);
121
- expect(client).toHaveBeenCalledWith('/foo', {
122
- body: { data: { ok: true } },
123
- method: 'POST',
124
- });
125
- });
126
-
127
- it('should send PUT request when existing row', async () => {
128
- const client = createClient().mockResolvedValue({});
129
- const model = createModel('http://1.2.3.4', {}, 'foo');
130
- const result = await model.upsert({
131
- where: { id: 1 },
132
- update: { ok: true },
133
- });
134
- expect(result).toEqual({});
135
- expect(client).toHaveBeenCalledTimes(1);
136
- expect(client).toHaveBeenCalledWith('/foo/1', {
137
- body: {
138
- where: { id: 1 },
139
- data: { ok: true },
140
- },
141
- method: 'PUT',
142
- });
143
- });
144
- });
145
-
146
- describe('model.delete(id)', () => {
147
- it('should send POST request', async () => {
148
- const client = createClient().mockResolvedValue({});
149
- const model = createModel('http://1.2.3.4', {}, 'foo');
150
- expect(await model.delete(1)).toEqual({});
151
- expect(client).toHaveBeenCalledTimes(1);
152
- expect(client).toHaveBeenCalledWith('/foo/1', {
153
- method: 'DELETE',
154
- });
155
- });
156
- });
157
-
158
- describe('model.validateAttr(key, value)', () => {
159
- it('should not validate when no schema', () => {
160
- const model = createModel('http://1.2.3.4', {}, 'foo');
161
- expect(model.validateAttr('a', 1)).toBeUndefined();
162
- });
163
-
164
- it('should validate text when required', () => {
165
- const schema = {
166
- properties: {
167
- a: { type: 'string' },
168
- },
169
- };
170
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
171
- expect(model.validateAttr('a', '')).toBe('Field is required');
172
- });
173
-
174
- it('should validate number when required', () => {
175
- const schema = {
176
- properties: {
177
- a: { type: 'integer', required: true },
178
- },
179
- };
180
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
181
- expect(model.validateAttr('a', 'foo')).toBe('Invalid number format');
182
- });
183
-
184
- it('should validate enum', () => {
185
- const schema = {
186
- properties: {
187
- a: { type: 'integer', enum: [1, 2, 3] },
188
- },
189
- };
190
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
191
- expect(model.validateAttr('a', 'foo')).toBe('foo is not one of: 1,2,3');
192
- });
193
-
194
- it('should allow empty enum when not required', () => {
195
- const schema = {
196
- properties: {
197
- a: { type: ['integer', 'null'], enum: [1, 2, 3] },
198
- },
199
- };
200
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
201
- expect(model.validateAttr('a', null)).toBeUndefined();
202
- expect(model.validateAttr('a', undefined)).toBeUndefined();
203
- expect(model.validateAttr('a', '')).toBeUndefined();
204
- });
205
- });
206
-
207
- describe('model.validate(attributes)', () => {
208
- it('should not validate when no schema', () => {
209
- const model = createModel('http://1.2.3.4', {}, 'foo');
210
- expect(model.validate({ a: 1 })).toEqual({});
211
- });
212
-
213
- it('should validate when schema present', () => {
214
- const schema = {
215
- properties: {
216
- a: { type: 'string' },
217
- b: { type: 'integer' },
218
- },
219
- };
220
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
221
- expect(model.validate({ a: '', b: 1 })).toEqual({ a: 'Field is required' });
222
- });
223
-
224
- it('should validate number when required', () => {
225
- const schema = {
226
- properties: {
227
- a: { type: 'integer' },
228
- },
229
- };
230
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
231
- expect(model.validateAttr('a', 'foo')).toBe('Invalid number format');
232
- });
233
-
234
- it('should validate enum', () => {
235
- const schema = {
236
- properties: {
237
- a: { type: 'integer', enum: [1, 2, 3] },
238
- },
239
- };
240
- const model = createModel('http://1.2.3.4', {}, 'foo', schema);
241
- expect(model.validateAttr('a', 'foo')).toBe('foo is not one of: 1,2,3');
242
- });
243
- });
package/lib/team.js DELETED
@@ -1,52 +0,0 @@
1
- import { createModels, extendSchemaDefinitions } from './model';
2
- import currencies from './currencies';
3
- import roles from './roles';
4
- import schema from '../schemas/team.json';
5
-
6
- const extended = {
7
- Country: {
8
- plural: 'countries',
9
- },
10
- Contract: {
11
- inputProps: [
12
- 'uid',
13
- 'countryCode',
14
- 'currency',
15
- 'isEmployment',
16
- 'feeBasis',
17
- 'feeAmount',
18
- 'hoursPerWeek',
19
- 'start',
20
- 'end',
21
- ],
22
- properties: {
23
- countryCode: {
24
- enum: ['BR', 'CL', 'CO', 'MX', 'PE'],
25
- },
26
- currency: {
27
- enum: currencies,
28
- },
29
- },
30
- },
31
- Gig: {
32
- inputProps: [
33
- // 'contract',
34
- 'cohortId',
35
- 'uid',
36
- 'role',
37
- 'hoursPerWeek',
38
- 'start',
39
- 'end',
40
- ],
41
- properties: {
42
- role: {
43
- enum: roles,
44
- },
45
- },
46
- },
47
- };
48
-
49
- export const createAPI = (url, state) => createModels(url, state, {
50
- ...schema,
51
- definitions: extendSchemaDefinitions(schema, extended),
52
- });
package/schemas/team.json DELETED
@@ -1,146 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "definitions": {
4
- "Country": {
5
- "type": "object",
6
- "properties": {
7
- "code": {
8
- "type": "string"
9
- },
10
- "name": {
11
- "type": "string"
12
- },
13
- "contracts": {
14
- "type": "array",
15
- "items": {
16
- "$ref": "#/definitions/Contract"
17
- }
18
- }
19
- }
20
- },
21
- "Contract": {
22
- "type": "object",
23
- "properties": {
24
- "id": {
25
- "type": "integer"
26
- },
27
- "createdAt": {
28
- "type": "string",
29
- "format": "date-time"
30
- },
31
- "updatedAt": {
32
- "type": "string",
33
- "format": "date-time"
34
- },
35
- "createdBy": {
36
- "type": "string"
37
- },
38
- "uid": {
39
- "type": "string"
40
- },
41
- "isEmployment": {
42
- "type": "boolean"
43
- },
44
- "currency": {
45
- "type": "string"
46
- },
47
- "feeBasis": {
48
- "type": "string",
49
- "enum": [
50
- "HOURLY",
51
- "MONTHLY"
52
- ]
53
- },
54
- "feeAmount": {
55
- "type": "integer"
56
- },
57
- "hoursPerWeek": {
58
- "type": "integer"
59
- },
60
- "start": {
61
- "type": "string",
62
- "format": "date-time"
63
- },
64
- "end": {
65
- "type": [
66
- "string",
67
- "null"
68
- ],
69
- "format": "date-time"
70
- },
71
- "country": {
72
- "$ref": "#/definitions/Country"
73
- },
74
- "gigs": {
75
- "type": "array",
76
- "items": {
77
- "$ref": "#/definitions/Gig"
78
- }
79
- }
80
- }
81
- },
82
- "Gig": {
83
- "type": "object",
84
- "properties": {
85
- "id": {
86
- "type": "integer"
87
- },
88
- "createdAt": {
89
- "type": "string",
90
- "format": "date-time"
91
- },
92
- "updatedAt": {
93
- "type": "string",
94
- "format": "date-time"
95
- },
96
- "createdBy": {
97
- "type": "string"
98
- },
99
- "role": {
100
- "type": "string",
101
- "enum": [
102
- "bm",
103
- "pdc",
104
- "js",
105
- "ux"
106
- ]
107
- },
108
- "hoursPerWeek": {
109
- "type": "integer"
110
- },
111
- "start": {
112
- "type": "string",
113
- "format": "date-time"
114
- },
115
- "end": {
116
- "type": [
117
- "string",
118
- "null"
119
- ],
120
- "format": "date-time"
121
- },
122
- "uid": {
123
- "type": "string"
124
- },
125
- "cohortId": {
126
- "type": "integer"
127
- },
128
- "contract": {
129
- "$ref": "#/definitions/Contract"
130
- }
131
- }
132
- }
133
- },
134
- "type": "object",
135
- "properties": {
136
- "country": {
137
- "$ref": "#/definitions/Country"
138
- },
139
- "contract": {
140
- "$ref": "#/definitions/Contract"
141
- },
142
- "gig": {
143
- "$ref": "#/definitions/Gig"
144
- }
145
- }
146
- }