@laboratoria/sdk-js 0.1.1 → 1.2.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
@@ -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,20 @@
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
+ primaryKey: 'code',
8
+ plural: 'countries',
9
+ searchProps: ['code', 'name'],
10
+ },
5
11
  User: {
6
12
  primaryKey: 'uid',
7
13
  inputProps: [
8
14
  'firstName',
9
15
  'lastName',
10
16
  'email',
11
- 'locale',
17
+ 'lang',
12
18
  'github',
13
19
  'linkedin',
14
20
  'bio',
@@ -22,17 +28,24 @@ const extended = {
22
28
  email: {
23
29
  inputType: 'email',
24
30
  },
25
- locale: {
26
- enum: ['es-ES', 'pt-BR'],
27
- },
28
31
  bio: {
29
32
  inputType: 'textarea',
30
33
  },
31
34
  },
32
35
  parse: (props) => {
36
+ const avatar = (
37
+ props.github
38
+ ? `https://github.com/${props.github}.png?size=`
39
+ : `https://www.gravatar.com/avatar/${md5(props.email)}?s=`
40
+ )
33
41
  return {
34
42
  ...props,
35
43
  fullName: `${props.firstName} ${props.lastName}`,
44
+ avatar: size => `${avatar}${size}`,
45
+ isStaff: ['staff', 'manager', 'finance', 'admin'].includes(props.role),
46
+ isManager: ['manager', 'finance', 'admin'].includes(props.role),
47
+ isFinance: ['finance', 'admin'].includes(props.role),
48
+ isAdmin: props.role === 'admin',
36
49
  };
37
50
  },
38
51
  },
@@ -59,6 +72,27 @@ const extended = {
59
72
  `${name}${!stage ? '' : ` - ${stage.name}`} (id: ${id})`
60
73
  ),
61
74
  properties: {},
75
+ parse: (props) => {
76
+ const now = Date.now();
77
+ // TODO: Handle case where start and/or end have not been selected in query
78
+ const status = (
79
+ props.start > now
80
+ ? 'upcoming'
81
+ : props.end < now
82
+ ? 'past'
83
+ : 'active'
84
+ );
85
+ const size = (
86
+ status === 'upcoming' && props.vacancies
87
+ ? props.vacancies
88
+ : props._count?.students || props.students?.length || 0
89
+ );
90
+ return {
91
+ ...props,
92
+ status,
93
+ size,
94
+ };
95
+ },
62
96
  },
63
97
  Student: {
64
98
  inputProps: [],
@@ -71,8 +105,8 @@ const extended = {
71
105
  'date',
72
106
  'reason',
73
107
  'reasonDetail',
74
- 'project',
75
- 'stage',
108
+ 'lastProject',
109
+ 'completedProjects',
76
110
  'notes',
77
111
  'staffSad',
78
112
  'covidRelated',
@@ -85,14 +119,35 @@ const extended = {
85
119
  'cohort.name',
86
120
  ],
87
121
  },
122
+ Contract: {
123
+ inputProps: [
124
+ 'user',
125
+ 'country',
126
+ 'currency',
127
+ 'isEmployment',
128
+ 'feeBasis',
129
+ 'feeAmount',
130
+ 'hoursPerWeek',
131
+ 'start',
132
+ 'end',
133
+ ],
134
+ searchProps: ['user.firstName'],
135
+ getOptionLabel: ({ id, user }) => `${user?.firstName} (id: ${id})`,
136
+ },
137
+ Gig: {
138
+ inputProps: [
139
+ 'cohort',
140
+ 'user',
141
+ 'contract',
142
+ 'role',
143
+ 'hoursPerWeek',
144
+ 'start',
145
+ 'end',
146
+ ],
147
+ },
88
148
  };
89
149
 
90
150
  export const createAPI = (url, state) => createModels(url, state, {
91
151
  ...schema,
92
152
  definitions: extendSchemaDefinitions(schema, extended),
93
153
  });
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,43 +103,42 @@ 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
+ return 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
+ };
86
124
 
87
125
 
88
- const createModel = (baseUrl, state, collectionName, schema = {}) => {
126
+ export const createModel = (baseUrl, state, collectionName, schema = {}) => {
127
+ const primaryKey = schema.primaryKey || 'id';
89
128
  const validator = createValidator(schema);
90
129
  const buildURL = createBuildURL(collectionName);
91
- const defaultInclude = buildDefaultInclude(schema);
92
- const buildQuery = createBuildQuery(defaultInclude);
93
130
  const req = (...args) => createClient(baseUrl, state.authUser)(...args);
94
- const create = q => req(buildURL(), {
131
+ const create = ({ data, ...q }) => req(buildURL(null, q), {
95
132
  method: 'POST',
96
- body: buildQuery(q),
97
- });
98
-
99
- const put = q => req(buildURL(q.where.id), {
100
- method: 'PUT',
101
- body: buildQuery(q),
133
+ body: serializeData(data, schema),
102
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
+ };
103
142
 
104
143
  const parse = data => {
105
144
  const parsed = Object.keys(data).reduce(
@@ -123,23 +162,53 @@ const createModel = (baseUrl, state, collectionName, schema = {}) => {
123
162
  );
124
163
  };
125
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
+
126
195
  return {
127
196
  get schema() {
128
- return schema;
197
+ return { ...schema, primaryKey };
129
198
  },
130
- get include() {
131
- return defaultInclude;
199
+ get relations() {
200
+ return relations;
132
201
  },
133
202
  validateAttr: validator.validateAttr,
134
203
  validate: validator.validate,
135
- findMany: q => req(buildURL(null, buildQuery(q)))
204
+ findMany: q => req(buildURL(null, q))
136
205
  .then(results => results.map(parse)),
137
- findById: (id, q) => req(buildURL(id, buildQuery(q)))
206
+ findById: (id, q) => req(buildURL(id, q))
138
207
  .then(raw => !!raw ? parse(raw) : raw),
139
208
  create,
140
209
  update: put,
141
210
  upsert: opts => (
142
- !opts.where.id
211
+ !opts.where[primaryKey]
143
212
  ? create({ data: opts.create })
144
213
  : put({ where: opts.where, data: opts.update })
145
214
  ),
@@ -147,9 +216,6 @@ const createModel = (baseUrl, state, collectionName, schema = {}) => {
147
216
  };
148
217
  };
149
218
 
150
-
151
- export default createModel;
152
-
153
219
  export const createModels = (url, state, schema) => {
154
220
  return Object.keys(schema.properties).reduce(
155
221
  (prev, key) => {
package/package.json CHANGED
@@ -1,23 +1,33 @@
1
1
  {
2
2
  "name": "@laboratoria/sdk-js",
3
- "version": "0.1.1",
3
+ "version": "1.2.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.17.0",
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
+ }