@laboratoria/sdk-js 0.1.0 → 1.1.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.
Files changed (40) hide show
  1. package/README.md +40 -3
  2. package/index.js +12 -20
  3. package/lib/core.js +67 -11
  4. package/lib/curriculum.js +59 -0
  5. package/lib/model.js +108 -36
  6. package/package.json +21 -11
  7. package/schemas/core.json +211 -11
  8. package/coverage/clover.xml +0 -140
  9. package/coverage/coverage-final.json +0 -9
  10. package/coverage/lcov-report/base.css +0 -224
  11. package/coverage/lcov-report/block-navigation.js +0 -79
  12. package/coverage/lcov-report/client.js.html +0 -152
  13. package/coverage/lcov-report/core.js.html +0 -281
  14. package/coverage/lcov-report/favicon.png +0 -0
  15. package/coverage/lcov-report/index.html +0 -126
  16. package/coverage/lcov-report/model.js.html +0 -557
  17. package/coverage/lcov-report/prettify.css +0 -1
  18. package/coverage/lcov-report/prettify.js +0 -2
  19. package/coverage/lcov-report/sdk-js/index.html +0 -111
  20. package/coverage/lcov-report/sdk-js/index.js.html +0 -293
  21. package/coverage/lcov-report/sdk-js/lib/campuses.js.html +0 -146
  22. package/coverage/lcov-report/sdk-js/lib/client.js.html +0 -152
  23. package/coverage/lcov-report/sdk-js/lib/core.js.html +0 -374
  24. package/coverage/lcov-report/sdk-js/lib/currencies.js.html +0 -89
  25. package/coverage/lcov-report/sdk-js/lib/index.html +0 -201
  26. package/coverage/lcov-report/sdk-js/lib/jobs.js.html +0 -143
  27. package/coverage/lcov-report/sdk-js/lib/model.js.html +0 -665
  28. package/coverage/lcov-report/sdk-js/lib/roles.js.html +0 -110
  29. package/coverage/lcov-report/sdk-js/lib/schemas.js.html +0 -434
  30. package/coverage/lcov-report/sdk-js/lib/team.js.html +0 -233
  31. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  32. package/coverage/lcov-report/sorter.js +0 -170
  33. package/coverage/lcov-report/team.js.html +0 -209
  34. package/coverage/lcov.info +0 -359
  35. package/index.spec.js +0 -112
  36. package/lib/client.spec.js +0 -77
  37. package/lib/currencies.js +0 -3
  38. package/lib/model.spec.js +0 -215
  39. package/lib/team.js +0 -52
  40. package/schemas/team.json +0 -146
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
@@ -5,16 +5,14 @@ import {
5
5
  signInWithEmailAndPassword,
6
6
  signOut,
7
7
  } from 'firebase/auth';
8
- import { createClient } from './lib/client';
9
8
  import { createAPI as createCoreAPI } from './lib/core';
9
+ import { createAPI as createCurriculumAPI } from './lib/curriculum';
10
10
  import { createAPI as createJobsAPI } from './lib/jobs';
11
- import { createAPI as createTeamAPI } from './lib/team';
12
11
 
13
12
  export const createApp = ({
14
13
  firebaseApiKey = 'AIzaSyAXbaEbpq8NOfn0r8mIrcoHvoGRkJThwdc',
15
14
  firebaseProject = 'laboratoria-la',
16
15
  coreApiUrl = 'https://us-central1-outpost-272720.cloudfunctions.net/core-api',
17
- teamApiUrl = 'https://us-central1-outpost-272720.cloudfunctions.net/team-api',
18
16
  jobsApiUrl = 'https://us-central1-outpost-272720.cloudfunctions.net/jobs-api',
19
17
  } = {}) => {
20
18
  const firebaseApp = initializeApp({
@@ -26,31 +24,25 @@ export const createApp = ({
26
24
 
27
25
  const firebaseAuth = getAuth(firebaseApp);
28
26
  const state = { authUser: undefined, user: undefined };
29
-
30
- const setState = (authUser, user) => {
31
- Object.assign(state, { authUser, user });
32
- };
27
+ const coreAPI = createCoreAPI(coreApiUrl, state);
28
+ const jobsAPI = createJobsAPI(jobsApiUrl, state);
29
+ const curriculumAPI = createCurriculumAPI();
33
30
 
34
31
  const authAPI = {
35
32
  onChange: fn => onAuthStateChanged(firebaseAuth, (authUser) => {
36
33
  if (!authUser) {
37
- setState(null, null);
34
+ Object.assign(state, { authUser: null, user: null });
38
35
  return fn(state);
39
36
  }
40
- createClient(coreApiUrl, authUser)(`/users/${authUser.uid}`)
37
+ Object.assign(state, { authUser });
38
+ coreAPI.user.findById(authUser.uid)
41
39
  .then((user) => {
42
- Object.assign(user, {
43
- isStaff: ['staff', 'manager', 'finance', 'admin'].includes(user.role),
44
- isManager: ['manager', 'finance', 'admin'].includes(user.role),
45
- isFinance: ['finance', 'admin'].includes(user.role),
46
- isAdmin: user.role === 'admin',
47
- });
48
- setState(authUser, user);
40
+ Object.assign(state, { user });
49
41
  fn(state);
50
42
  })
51
43
  .catch((err) => {
52
44
  console.error(err);
53
- setState(null, null)
45
+ Object.assign(state, { authUser: null, user: null });
54
46
  fn(state);
55
47
  });
56
48
  }),
@@ -63,9 +55,9 @@ export const createApp = ({
63
55
  };
64
56
 
65
57
  return {
58
+ ...jobsAPI,
59
+ ...curriculumAPI,
60
+ ...coreAPI,
66
61
  auth: authAPI,
67
- ...createJobsAPI(jobsApiUrl, state),
68
- ...createTeamAPI(teamApiUrl, state),
69
- ...createCoreAPI(coreApiUrl, state),
70
62
  };
71
63
  };
package/lib/core.js CHANGED
@@ -1,14 +1,18 @@
1
+ import md5 from 'blueimp-md5';
1
2
  import { createModels, extendSchemaDefinitions } from './model';
2
3
  import schema from '../schemas/core.json';
3
4
 
4
5
  const extended = {
6
+ Country: {
7
+ plural: 'countries',
8
+ },
5
9
  User: {
6
10
  primaryKey: 'uid',
7
11
  inputProps: [
8
12
  'firstName',
9
13
  'lastName',
10
14
  'email',
11
- 'locale',
15
+ 'lang',
12
16
  'github',
13
17
  'linkedin',
14
18
  'bio',
@@ -22,17 +26,24 @@ const extended = {
22
26
  email: {
23
27
  inputType: 'email',
24
28
  },
25
- locale: {
26
- enum: ['es-ES', 'pt-BR'],
27
- },
28
29
  bio: {
29
30
  inputType: 'textarea',
30
31
  },
31
32
  },
32
33
  parse: (props) => {
34
+ const avatar = (
35
+ props.github
36
+ ? `https://github.com/${props.github}.png?size=`
37
+ : `https://www.gravatar.com/avatar/${md5(props.email)}?s=`
38
+ )
33
39
  return {
34
40
  ...props,
35
41
  fullName: `${props.firstName} ${props.lastName}`,
42
+ avatar: size => `${avatar}${size}`,
43
+ isStaff: ['staff', 'manager', 'finance', 'admin'].includes(props.role),
44
+ isManager: ['manager', 'finance', 'admin'].includes(props.role),
45
+ isFinance: ['finance', 'admin'].includes(props.role),
46
+ isAdmin: props.role === 'admin',
36
47
  };
37
48
  },
38
49
  },
@@ -59,6 +70,27 @@ const extended = {
59
70
  `${name}${!stage ? '' : ` - ${stage.name}`} (id: ${id})`
60
71
  ),
61
72
  properties: {},
73
+ parse: (props) => {
74
+ const now = Date.now();
75
+ // TODO: Handle case where start and/or end have not been selected in query
76
+ const status = (
77
+ props.start > now
78
+ ? 'upcoming'
79
+ : props.end < now
80
+ ? 'past'
81
+ : 'active'
82
+ );
83
+ const size = (
84
+ status === 'upcoming' && props.vacancies
85
+ ? props.vacancies
86
+ : props._count?.students || props.students?.length || 0
87
+ );
88
+ return {
89
+ ...props,
90
+ status,
91
+ size,
92
+ };
93
+ },
62
94
  },
63
95
  Student: {
64
96
  inputProps: [],
@@ -71,8 +103,8 @@ const extended = {
71
103
  'date',
72
104
  'reason',
73
105
  'reasonDetail',
74
- 'project',
75
- 'stage',
106
+ 'lastProject',
107
+ 'completedProjects',
76
108
  'notes',
77
109
  'staffSad',
78
110
  'covidRelated',
@@ -85,14 +117,38 @@ const extended = {
85
117
  'cohort.name',
86
118
  ],
87
119
  },
120
+ Contract: {
121
+ inputProps: [
122
+ 'user',
123
+ 'countryCode',
124
+ 'currency',
125
+ 'isEmployment',
126
+ 'feeBasis',
127
+ 'feeAmount',
128
+ 'hoursPerWeek',
129
+ 'start',
130
+ 'end',
131
+ ],
132
+ properties: {
133
+ countryCode: {
134
+ enum: ['BR', 'CL', 'CO', 'MX', 'PE'],
135
+ },
136
+ },
137
+ },
138
+ Gig: {
139
+ inputProps: [
140
+ // 'contract',
141
+ 'cohort',
142
+ 'user',
143
+ 'role',
144
+ 'hoursPerWeek',
145
+ 'start',
146
+ 'end',
147
+ ],
148
+ },
88
149
  };
89
150
 
90
151
  export const createAPI = (url, state) => createModels(url, state, {
91
152
  ...schema,
92
153
  definitions: extendSchemaDefinitions(schema, extended),
93
154
  });
94
-
95
- // const {
96
- // delete: _,
97
- // ...userAPI,
98
- // } = 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,13 +1,53 @@
1
1
  import { createClient } from './client.js';
2
2
 
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
+
3
36
  const createValidator = (schema) => {
4
37
  const properties = schema.properties || {};
5
38
  const inputProps = schema.inputProps || Object.keys(properties);
6
39
 
7
40
  const validateAttr = (key, value) => {
8
41
  const attrSchema = properties[key] || {};
9
- const type = Array.isArray(attrSchema.type) ? attrSchema.type[0] : attrSchema.type;
10
- const isRequired = !Array.isArray(attrSchema.type) || !attrSchema.type.includes('null');
42
+ const type = (
43
+ Array.isArray(attrSchema.type)
44
+ ? attrSchema.type[0]
45
+ : attrSchema.type
46
+ );
47
+ const isRequired = (
48
+ !Array.isArray(attrSchema.type)
49
+ || !attrSchema.type.includes('null')
50
+ );
11
51
 
12
52
  if (attrSchema.enum) {
13
53
  if (!isRequired && !value) {
@@ -63,42 +103,47 @@ const createBuildURL = collectionName => (id, q) => {
63
103
  };
64
104
 
65
105
 
66
- const createBuildQuery = defaultInclude => (q = {}) => ({
67
- ...(!q.select && Object.keys(defaultInclude).length && { include: defaultInclude }),
68
- ...q,
69
- });
70
-
71
-
72
- const buildDefaultInclude = schema => Object.keys(schema.properties || {}).reduce(
73
- (memo, key) => (
74
- (
75
- schema.properties[key].$ref
76
- || (
77
- Array.isArray(schema.properties[key].anyOf)
78
- && schema.properties[key].anyOf[0].$ref
79
- )
80
- )
81
- ? { ...memo, [key]: true }
82
- : memo
83
- ),
84
- {},
85
- );
106
+ const serializeData = (data, schema) => {
107
+ const hasInputProps = Array.isArray(schema.inputProps);
108
+ const payload = Object.keys(data).reduce(
109
+ (memo, key) => {
110
+ if (hasInputProps && !schema.inputProps.includes(key)) {
111
+ return memo;
112
+ }
113
+ if (isOptionalOneToOneRelation(schema, key) && data[key] === null) {
114
+ return memo;
115
+ }
116
+ return {
117
+ ...memo,
118
+ [key]: data[key],
119
+ };
120
+ },
121
+ {},
122
+ );
123
+
124
+ // {
125
+ // signupCohort: {
126
+ // connect: { id: signupCohort.id },
127
+ // },
128
+ // }
129
+
130
+ return payload;
131
+ };
86
132
 
87
133
 
88
- const createModel = (baseUrl, state, collectionName, schema = {}) => {
134
+ export const createModel = (baseUrl, state, collectionName, schema = {}) => {
135
+ const primaryKey = schema.primaryKey || 'id';
89
136
  const validator = createValidator(schema);
90
137
  const buildURL = createBuildURL(collectionName);
91
- const defaultInclude = buildDefaultInclude(schema);
92
- const buildQuery = createBuildQuery(defaultInclude);
93
138
  const req = (...args) => createClient(baseUrl, state.authUser)(...args);
94
139
  const create = q => req(buildURL(), {
95
140
  method: 'POST',
96
- body: buildQuery(q),
141
+ body: Object.assign(q, { data: serializeData(q.data, schema) }),
97
142
  });
98
143
 
99
- const put = q => req(buildURL(q.where.id), {
144
+ const put = q => req(buildURL(q.where[primaryKey]), {
100
145
  method: 'PUT',
101
- body: buildQuery(q),
146
+ body: Object.assign(q, { data: serializeData(q.data, schema) }),
102
147
  });
103
148
 
104
149
  const parse = data => {
@@ -123,23 +168,53 @@ const createModel = (baseUrl, state, collectionName, schema = {}) => {
123
168
  );
124
169
  };
125
170
 
171
+ const relations = Object.keys(schema.properties || {}).reduce(
172
+ (memo, key) => (
173
+ isRequiredOneToOneRelation(schema, key)
174
+ ? Object.assign(memo, {
175
+ all: memo.all.concat(key),
176
+ oneToOne: memo.oneToOne.concat(key),
177
+ requiredOneToOne: memo.requiredOneToOne.concat(key),
178
+ })
179
+ : isOptionalOneToOneRelation(schema, key)
180
+ ? Object.assign(memo, {
181
+ all: memo.all.concat(key),
182
+ oneToOne: memo.oneToOne.concat(key),
183
+ optionalOneToOne: memo.optionalOneToOne.concat(key),
184
+ })
185
+ : isOneToManyRelation(schema, key)
186
+ ? Object.assign(memo, {
187
+ all: memo.all.concat(key),
188
+ oneToMany: memo.oneToMany.concat(key),
189
+ })
190
+ : memo
191
+ ),
192
+ {
193
+ all: [],
194
+ oneToOne: [],
195
+ requiredOneToOne: [],
196
+ optionalOneToOne: [],
197
+ oneToMany: [],
198
+ },
199
+ );
200
+
126
201
  return {
127
202
  get schema() {
128
203
  return schema;
129
204
  },
130
- get include() {
131
- return defaultInclude;
205
+ get relations() {
206
+ return relations;
132
207
  },
133
208
  validateAttr: validator.validateAttr,
134
209
  validate: validator.validate,
135
- findMany: q => req(buildURL(null, buildQuery(q)))
210
+ findMany: q => req(buildURL(null, q))
136
211
  .then(results => results.map(parse)),
137
- findById: (id, q) => req(buildURL(id, buildQuery(q)))
212
+ findById: (id, q) => req(buildURL(id, q))
138
213
  .then(raw => !!raw ? parse(raw) : raw),
139
214
  create,
140
215
  update: put,
141
216
  upsert: opts => (
142
- !opts.where.id
217
+ !opts.where[primaryKey]
143
218
  ? create({ data: opts.create })
144
219
  : put({ where: opts.where, data: opts.update })
145
220
  ),
@@ -147,9 +222,6 @@ const createModel = (baseUrl, state, collectionName, schema = {}) => {
147
222
  };
148
223
  };
149
224
 
150
-
151
- export default createModel;
152
-
153
225
  export const createModels = (url, state, schema) => {
154
226
  return Object.keys(schema.properties).reduce(
155
227
  (prev, key) => {
package/package.json CHANGED
@@ -1,23 +1,33 @@
1
1
  {
2
2
  "name": "@laboratoria/sdk-js",
3
- "version": "0.1.0",
3
+ "version": "1.1.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
- "firebase": "^9.5.0"
11
+ "blueimp-md5": "^2.19.0",
12
+ "firebase": "^9.6.5"
11
13
  },
12
14
  "devDependencies": {
13
- "@babel/core": "^7.16.0",
14
- "@babel/plugin-transform-modules-commonjs": "^7.16.0",
15
- "babel-jest": "^27.4.2",
16
- "jest": "^27.4.3",
17
- "webpack": "^5.64.4",
18
- "webpack-cli": "^4.9.1"
15
+ "@babel/core": "^7.16.12",
16
+ "@babel/plugin-transform-modules-commonjs": "^7.16.8",
17
+ "babel-jest": "^27.4.6",
18
+ "jest": "^27.4.7",
19
+ "webpack": "^5.68.0",
20
+ "webpack-cli": "^4.9.2"
19
21
  },
20
22
  "jest": {
21
- "testEnvironment": "jsdom"
23
+ "testEnvironment": "jsdom",
24
+ "coverageThreshold": {
25
+ "global": {
26
+ "statements": 97,
27
+ "branches": 92,
28
+ "functions": 98,
29
+ "lines": 97
30
+ }
31
+ }
22
32
  }
23
- }
33
+ }