@vue-skuilder/express 0.1.1

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 (78) hide show
  1. package/.env.development +9 -0
  2. package/.prettierignore +4 -0
  3. package/.prettierrc +8 -0
  4. package/.vscode/launch.json +20 -0
  5. package/assets/classroomDesignDoc.js +24 -0
  6. package/assets/courseValidateDocUpdate.js +56 -0
  7. package/assets/get-tagsDesignDoc.json +9 -0
  8. package/babel.config.js +6 -0
  9. package/dist/app.d.ts +6 -0
  10. package/dist/app.d.ts.map +1 -0
  11. package/dist/app.js +194 -0
  12. package/dist/app.js.map +1 -0
  13. package/dist/attachment-preprocessing/index.d.ts +11 -0
  14. package/dist/attachment-preprocessing/index.d.ts.map +1 -0
  15. package/dist/attachment-preprocessing/index.js +146 -0
  16. package/dist/attachment-preprocessing/index.js.map +1 -0
  17. package/dist/attachment-preprocessing/normalize.d.ts +7 -0
  18. package/dist/attachment-preprocessing/normalize.d.ts.map +1 -0
  19. package/dist/attachment-preprocessing/normalize.js +90 -0
  20. package/dist/attachment-preprocessing/normalize.js.map +1 -0
  21. package/dist/client-requests/classroom-requests.d.ts +26 -0
  22. package/dist/client-requests/classroom-requests.d.ts.map +1 -0
  23. package/dist/client-requests/classroom-requests.js +171 -0
  24. package/dist/client-requests/classroom-requests.js.map +1 -0
  25. package/dist/client-requests/course-requests.d.ts +10 -0
  26. package/dist/client-requests/course-requests.d.ts.map +1 -0
  27. package/dist/client-requests/course-requests.js +135 -0
  28. package/dist/client-requests/course-requests.js.map +1 -0
  29. package/dist/client.d.ts +31 -0
  30. package/dist/client.d.ts.map +1 -0
  31. package/dist/client.js +70 -0
  32. package/dist/client.js.map +1 -0
  33. package/dist/couchdb/authentication.d.ts +4 -0
  34. package/dist/couchdb/authentication.d.ts.map +1 -0
  35. package/dist/couchdb/authentication.js +64 -0
  36. package/dist/couchdb/authentication.js.map +1 -0
  37. package/dist/couchdb/index.d.ts +18 -0
  38. package/dist/couchdb/index.d.ts.map +1 -0
  39. package/dist/couchdb/index.js +52 -0
  40. package/dist/couchdb/index.js.map +1 -0
  41. package/dist/design-docs.d.ts +63 -0
  42. package/dist/design-docs.d.ts.map +1 -0
  43. package/dist/design-docs.js +90 -0
  44. package/dist/design-docs.js.map +1 -0
  45. package/dist/logger.d.ts +3 -0
  46. package/dist/logger.d.ts.map +1 -0
  47. package/dist/logger.js +62 -0
  48. package/dist/logger.js.map +1 -0
  49. package/dist/routes/logs.d.ts +3 -0
  50. package/dist/routes/logs.d.ts.map +1 -0
  51. package/dist/routes/logs.js +274 -0
  52. package/dist/routes/logs.js.map +1 -0
  53. package/dist/utils/env.d.ts +10 -0
  54. package/dist/utils/env.d.ts.map +1 -0
  55. package/dist/utils/env.js +38 -0
  56. package/dist/utils/env.js.map +1 -0
  57. package/dist/utils/processQueue.d.ts +39 -0
  58. package/dist/utils/processQueue.d.ts.map +1 -0
  59. package/dist/utils/processQueue.js +175 -0
  60. package/dist/utils/processQueue.js.map +1 -0
  61. package/eslint.config.js +19 -0
  62. package/jest.config.ts +24 -0
  63. package/package.json +74 -0
  64. package/src/app.ts +246 -0
  65. package/src/attachment-preprocessing/index.ts +204 -0
  66. package/src/attachment-preprocessing/normalize.ts +123 -0
  67. package/src/client-requests/classroom-requests.ts +234 -0
  68. package/src/client-requests/course-requests.ts +188 -0
  69. package/src/client.ts +97 -0
  70. package/src/couchdb/authentication.ts +85 -0
  71. package/src/couchdb/index.ts +76 -0
  72. package/src/design-docs.ts +107 -0
  73. package/src/logger.ts +75 -0
  74. package/src/routes/logs.ts +289 -0
  75. package/src/utils/env.ts +51 -0
  76. package/src/utils/processQueue.ts +218 -0
  77. package/test/client.test.ts +144 -0
  78. package/tsconfig.json +27 -0
@@ -0,0 +1,204 @@
1
+ import CouchDB from '../couchdb/index.js';
2
+ import nano from 'nano';
3
+ import { normalize } from './normalize.js';
4
+ import AsyncProcessQueue, { Result } from '../utils/processQueue.js';
5
+ import logger from '../logger.js';
6
+ import { CourseLookup } from '@vue-skuilder/db';
7
+
8
+ // @ts-expect-error [todo]
9
+ const Q = new AsyncProcessQueue<AttachmentProcessingRequest, Result>(
10
+ processDocAttachments
11
+ );
12
+
13
+ interface DocForProcessing extends nano.DocumentGetResponse {
14
+ processed?: boolean | string[];
15
+ _attachments: {
16
+ [key: string]: {
17
+ content_type: string;
18
+ data?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
19
+ digest?: string;
20
+ length?: number;
21
+ revpos?: number;
22
+ stub?: boolean;
23
+ };
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Apply post-processing to a course database. Runs continuously.
29
+ * @param courseID
30
+ */
31
+ export function postProcessCourse(courseID: string): void {
32
+ try {
33
+ logger.info(`Following course ${courseID}`);
34
+
35
+ const crsString = `coursedb-${courseID}`;
36
+
37
+ // Get database instance
38
+ const db = CouchDB.use(crsString);
39
+
40
+ const courseFilter = filterFactory(courseID);
41
+
42
+ db.changesReader
43
+ .start({
44
+ // feed: 'continuous',
45
+ includeDocs: false,
46
+ })
47
+ .on('change', (change: nano.DatabaseChangesResultItem) => {
48
+ courseFilter(change).catch((e) => {
49
+ logger.error(`Error in CourseFilter for ${courseID}: ${e}`);
50
+ });
51
+ })
52
+ .on('error', (err: Error) => {
53
+ logger.error(`Error in changes feed for ${crsString}: ${err}`);
54
+ });
55
+ } catch (e) {
56
+ logger.error(`Error in postProcessCourse: ${e}`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Connect to CouchDB, monitor changes to uploaded card data,
62
+ * perform post-processing on uploaded media
63
+ */
64
+ export default async function postProcess(): Promise<void> {
65
+ try {
66
+ logger.info(`Following all course databases for changes...`);
67
+
68
+ const courses = await CourseLookup.allCourses();
69
+
70
+ for (const course of courses) {
71
+ try {
72
+ postProcessCourse(course._id);
73
+ } catch (e) {
74
+ logger.error(`Error processing course ${course._id}: ${e}`);
75
+ throw e;
76
+ }
77
+ }
78
+ } catch (e) {
79
+ logger.error(`Error in postProcess: ${e}`);
80
+ }
81
+ }
82
+
83
+ function filterFactory(courseID: string) {
84
+ const courseDatabase = CouchDB.use<DocForProcessing>(`coursedb-${courseID}`);
85
+
86
+ return async function filterChanges(
87
+ changeItem: nano.DatabaseChangesResultItem
88
+ ) {
89
+ try {
90
+ const docNoAttachments = await courseDatabase.get(changeItem.id, {
91
+ attachments: false,
92
+ });
93
+
94
+ if (
95
+ docNoAttachments._attachments &&
96
+ Object.keys(docNoAttachments._attachments).length > 0 &&
97
+ (docNoAttachments['processed'] === undefined ||
98
+ docNoAttachments['processed'] === false)
99
+ ) {
100
+ const doc = await courseDatabase.get(changeItem.id, {
101
+ attachments: true,
102
+ });
103
+ const processingRequest: AttachmentProcessingRequest = {
104
+ courseID,
105
+ docID: doc._id,
106
+ fields: [],
107
+ };
108
+ const atts = doc._attachments;
109
+ for (const attachment in atts) {
110
+ const content_type: string = atts[attachment]['content_type'];
111
+ logger.info(
112
+ `Course: ${courseID}\n\tAttachment ${attachment} in:\n\t${doc._id}\n should be processed...`
113
+ );
114
+
115
+ if (content_type.includes('audio')) {
116
+ processingRequest.fields.push({
117
+ name: attachment,
118
+ mimetype: content_type,
119
+ });
120
+ }
121
+ }
122
+ Q.addRequest(processingRequest);
123
+ }
124
+ } catch (e) {
125
+ logger.error(`Error processing doc ${changeItem.id}: ${e}`);
126
+ }
127
+ };
128
+ }
129
+
130
+ async function processDocAttachments(
131
+ request: AttachmentProcessingRequest
132
+ ): Promise<Result> {
133
+ if (request.fields.length == 0) {
134
+ logger.info(`No attachments to process for ${request.docID}`);
135
+ return {
136
+ error: 'No attachments to process',
137
+ ok: true,
138
+ status: 'warning',
139
+ };
140
+ }
141
+ const courseDatabase = CouchDB.use<DocForProcessing>(
142
+ `coursedb-${request.courseID}`
143
+ );
144
+
145
+ const doc = await courseDatabase.get(request.docID, {
146
+ attachments: true,
147
+ att_encoding_info: true,
148
+ });
149
+
150
+ for (const field of request.fields) {
151
+ logger.info(`Converting ${field.name}`);
152
+ const attachment = doc._attachments[field.name].data;
153
+ if (field.mimetype.includes('audio')) {
154
+ try {
155
+ const converted = await normalize(attachment);
156
+ field.returnData = converted;
157
+ } catch (e) {
158
+ logger.info(`Exception caught: ${e}`);
159
+ throw e;
160
+ }
161
+ }
162
+ }
163
+
164
+ logger.info('Conversions finished');
165
+
166
+ request.fields.forEach((field) => {
167
+ logger.info(`Replacing doc Data for ${field.name}`);
168
+ if (doc['processed']) {
169
+ (doc['processed'] as string[]).push(field.name);
170
+ } else {
171
+ doc['processed'] = [field.name];
172
+ }
173
+ doc._attachments[field.name].data = field.returnData;
174
+ });
175
+
176
+ // request was a noop.
177
+ // Mark as processed in order to avoid inifinte loop
178
+ if (request.fields.length === 0) {
179
+ doc['processed'] = true;
180
+ }
181
+
182
+ const resp = (await courseDatabase.insert(doc)) as unknown as Result;
183
+ resp.status = 'ok';
184
+
185
+ logger.info(`Processing request reinsert result: ${JSON.stringify(resp)}`);
186
+ return resp;
187
+ }
188
+
189
+ // interface _DatabaseChangesResultItemWithDoc
190
+ // extends nano.DatabaseChangesResultItem {
191
+ // doc: nano.DocumentGetResponse;
192
+ // courseID: string;
193
+ // }
194
+
195
+ interface AttachmentProcessingRequest {
196
+ courseID: string;
197
+ docID: string;
198
+ fields: ProcessingField[];
199
+ }
200
+ interface ProcessingField {
201
+ name: string;
202
+ mimetype: string;
203
+ returnData?: string;
204
+ }
@@ -0,0 +1,123 @@
1
+ import fs from 'fs';
2
+ import logger from '../logger.js';
3
+ import { promisify } from 'util';
4
+ import { exec as execCallback } from 'child_process';
5
+ const exec = promisify(execCallback);
6
+
7
+ import FFMPEGstatic from 'ffmpeg-static';
8
+ if (!FFMPEGstatic) {
9
+ const e = 'FFMPEGstatic executable not found';
10
+ logger.error(e);
11
+ throw new Error(e);
12
+ }
13
+
14
+ // string | null here - but we know it's a string from the above check
15
+ const FFMPEG = FFMPEGstatic as unknown as string;
16
+
17
+ logger.info(`FFMPEG path: ${FFMPEG}`);
18
+
19
+ checkFFMPEGVersion().catch((e) => {
20
+ const msg = 'FFMPEG version check failed';
21
+ logger.error(msg, e);
22
+ throw new Error(msg + e);
23
+ });
24
+
25
+ async function checkFFMPEGVersion() {
26
+ try {
27
+ if (!fs.existsSync(FFMPEG)) {
28
+ const e = `FFMPEG executable not found at path: ${FFMPEG}`;
29
+ logger.error(e);
30
+ throw new Error(e);
31
+ }
32
+
33
+ const result = await exec(`${FFMPEG} -version`);
34
+ const version = result.stdout.split('\n')[0];
35
+ logger.info(`FFMPEG version: ${version}`);
36
+
37
+ // Verify loudnorm filter availability
38
+ const filters = await exec(`${FFMPEG} -filters | grep loudnorm`);
39
+ if (!filters.stdout.includes('loudnorm')) {
40
+ throw new Error('loudnorm filter not available');
41
+ }
42
+ } catch (error) {
43
+ logger.error('FFMPEG version check failed:', error);
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * From FFMPEG's loudnorm output - loudness data on a media file
50
+ */
51
+ interface LoudnessData {
52
+ // these are numbers, but will be parsed as strings
53
+ input_i: string; //number;
54
+ input_tp: string; //number;
55
+ input_lra: string; //number;
56
+ input_thresh: string; //number;
57
+ output_i: string; //number;
58
+ output_tp: string; //number;
59
+ output_lra: string; //number;
60
+ output_thresh: string; //number;
61
+ normalization_type: string; // this one is actually a string
62
+ target_offset: string; //number;
63
+ }
64
+
65
+ /**
66
+ * Returns normalized, base-64 encoded mp3
67
+ *
68
+ * @param fileData the base-64 encoded mp3 data from couchdb
69
+ */
70
+ export async function normalize(fileData: string): Promise<string> {
71
+ const encoding = 'base64';
72
+ const tmpDir = fs.mkdtempSync(`audioNormalize-${encoding}-`);
73
+ const fileName = tmpDir + '/file.mp3';
74
+
75
+ fs.writeFileSync(fileName, fileData, {
76
+ encoding,
77
+ });
78
+
79
+ const ext = '.' + fileName.split('.')[1];
80
+
81
+ const PADDED = tmpDir + '/padded' + ext;
82
+ const PADDED_NORMALIZED = tmpDir + '/paddedNormalized' + ext;
83
+ const NORMALIZED = tmpDir + '/normalized' + ext;
84
+
85
+ try {
86
+ // elongate
87
+ await exec(FFMPEG + ` -i ${fileName} -af "adelay=10000|10000" ${PADDED}`);
88
+ const info = await exec(
89
+ FFMPEG +
90
+ ` -i ${PADDED} -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -`
91
+ );
92
+ const data: LoudnessData = JSON.parse(
93
+ info.stderr.substring(info.stderr.indexOf('{'))
94
+ );
95
+ // normalize the elongated file
96
+ await exec(
97
+ FFMPEG +
98
+ ` -i ${PADDED} -af ` +
99
+ `loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=${data.input_i}:` +
100
+ `measured_LRA=${data.input_lra}:measured_TP=${data.input_tp}:` +
101
+ `measured_thresh=${data.input_thresh}:offset=${data.target_offset}:linear=true:` +
102
+ `print_format=summary -ar 48k ${PADDED_NORMALIZED}`
103
+ );
104
+ // cut off the elongated part
105
+ await exec(
106
+ FFMPEG +
107
+ ` -i ${PADDED_NORMALIZED} -ss 00:00:10.000 -acodec copy ${NORMALIZED}`
108
+ );
109
+ const ret = fs.readFileSync(NORMALIZED, {
110
+ encoding,
111
+ });
112
+ return ret;
113
+ } catch (e) {
114
+ logger.error(e);
115
+ throw e;
116
+ } finally {
117
+ const files = fs.readdirSync(tmpDir);
118
+ files.forEach((file) => {
119
+ fs.unlinkSync(tmpDir + '/' + file);
120
+ });
121
+ fs.rmdirSync(tmpDir);
122
+ }
123
+ }
@@ -0,0 +1,234 @@
1
+ import hashids from 'hashids';
2
+ import Nano from 'nano';
3
+ import {
4
+ ClassroomConfig,
5
+ CreateClassroom,
6
+ JoinClassroom,
7
+ LeaveClassroom,
8
+ Status,
9
+ } from '@vue-skuilder/common';
10
+ import { classroomDbDesignDoc } from '../design-docs.js';
11
+ import CouchDB, {
12
+ SecurityObject,
13
+ docCount,
14
+ useOrCreateDB,
15
+ } from '../couchdb/index.js';
16
+ import AsyncProcessQueue, { Result } from '../utils/processQueue.js';
17
+ import logger from '../logger.js';
18
+
19
+ export const CLASSROOM_DB_LOOKUP = 'classdb-lookup';
20
+ const CLASSROOM_CONFIG = 'ClassroomConfig';
21
+
22
+ interface lookupData {
23
+ num: number;
24
+ uuid: string;
25
+ }
26
+
27
+ // async function deleteClassroom(_classroom_id: string) {}
28
+
29
+ async function getClassID(joinCode: string) {
30
+ try {
31
+ const doc = await (await useOrCreateDB(CLASSROOM_DB_LOOKUP)).get(joinCode);
32
+ return (doc as unknown as lookupData).uuid;
33
+ } catch {
34
+ return '';
35
+ }
36
+ }
37
+
38
+ async function getClassroomConfig(id: string): Promise<ClassroomConfig> {
39
+ return (await useOrCreateDB(getClassDBNames(id).studentDB)).get(
40
+ CLASSROOM_CONFIG
41
+ ) as unknown as ClassroomConfig;
42
+ }
43
+ async function writeClassroomConfig(config: ClassroomConfig, classID: string) {
44
+ logger.info(`Writing config for class: ${classID}`);
45
+ const dbNames = getClassDBNames(classID);
46
+ const studentDB = await useOrCreateDB(dbNames.studentDB);
47
+ const teacherDB = await useOrCreateDB(dbNames.teacherDB);
48
+
49
+ return Promise.all([
50
+ studentDB
51
+ .get(CLASSROOM_CONFIG)
52
+ .then((doc) => {
53
+ return studentDB.insert({
54
+ _id: CLASSROOM_CONFIG,
55
+ _rev: doc._rev,
56
+ ...config,
57
+ });
58
+ })
59
+ .catch((_err) => {
60
+ return studentDB.insert({
61
+ _id: CLASSROOM_CONFIG,
62
+ ...config,
63
+ });
64
+ }),
65
+ teacherDB
66
+ .get(CLASSROOM_CONFIG)
67
+ .then((doc) => {
68
+ return teacherDB.insert({
69
+ _id: CLASSROOM_CONFIG,
70
+ _rev: doc._rev,
71
+ ...config,
72
+ });
73
+ })
74
+ .catch((_err) => {
75
+ return teacherDB.insert({
76
+ _id: CLASSROOM_CONFIG,
77
+ ...config,
78
+ });
79
+ }),
80
+ ]);
81
+ }
82
+
83
+ function getClassDBNames(classId: string): {
84
+ studentDB: string;
85
+ teacherDB: string;
86
+ } {
87
+ return {
88
+ studentDB: `classdb-student-${classId}`,
89
+ teacherDB: `classdb-teacher-${classId}`,
90
+ };
91
+ }
92
+
93
+ async function createClassroom(config: ClassroomConfig) {
94
+ logger.info(`CreateClass Request:
95
+ ${JSON.stringify(config)}`);
96
+
97
+ const num = (await docCount(CLASSROOM_DB_LOOKUP)) + 1; //
98
+ const uuid = (await CouchDB.uuids(1)).uuids[0];
99
+ const hasher = new hashids('', 6, 'abcdefghijklmnopqrstuvwxyz123456789');
100
+ const studentDbName = `classdb-student-${uuid}`;
101
+ const teacherDbName = `classdb-teacher-${uuid}`;
102
+ config.joinCode = hasher.encode(num);
103
+
104
+ const security: SecurityObject = {
105
+ // _id: '_security',
106
+ admins: {
107
+ names: [],
108
+ roles: [],
109
+ },
110
+ members: {
111
+ names: config.teachers,
112
+ roles: [],
113
+ },
114
+ };
115
+
116
+ const [studentdb, teacherdb, lookup] = await Promise.all([
117
+ useOrCreateDB(studentDbName),
118
+ useOrCreateDB(teacherDbName),
119
+ useOrCreateDB('classdb-lookup'),
120
+ ]);
121
+ await Promise.all([
122
+ studentdb.insert(
123
+ {
124
+ validate_doc_update: classroomDbDesignDoc,
125
+ } as Nano.MaybeDocument,
126
+ '_design/_auth'
127
+ ),
128
+ // studentdb.insert(security, '_security'),
129
+ teacherdb.insert(security, '_security'),
130
+ lookup.insert(
131
+ {
132
+ num,
133
+ uuid,
134
+ } as Nano.MaybeDocument,
135
+ config.joinCode
136
+ ),
137
+ writeClassroomConfig(config, uuid),
138
+ ]);
139
+
140
+ const res: Result = {
141
+ ok: true,
142
+ status: 'ok',
143
+ };
144
+ const ret = {
145
+ joincode: config.joinCode,
146
+ uuid: uuid,
147
+ ...res,
148
+ };
149
+
150
+ logger.info(JSON.stringify(ret));
151
+
152
+ return ret;
153
+ }
154
+
155
+ async function leaveClassroom(
156
+ req: LeaveClassroom['data'] & { username: string }
157
+ ) {
158
+ const cfg: ClassroomConfig = await getClassroomConfig(req.classID);
159
+ if (cfg) {
160
+ const index = cfg.students.indexOf(req.username);
161
+ if (index !== -1) {
162
+ cfg.students.splice(index, 1);
163
+ }
164
+
165
+ await writeClassroomConfig(cfg, req.classID);
166
+
167
+ return {
168
+ status: Status.ok,
169
+ ok: true,
170
+ };
171
+ } else {
172
+ return {
173
+ status: Status.error,
174
+ ok: false,
175
+ errorText: 'Course with this ID not found.',
176
+ };
177
+ }
178
+ }
179
+
180
+ async function joinClassroom(req: JoinClassroom['data']) {
181
+ const classID = await getClassID(req.joinCode);
182
+ if (classID) {
183
+ const classDBNames = getClassDBNames(classID);
184
+
185
+ void (await useOrCreateDB(classDBNames.studentDB)).get('ClassroomConfig');
186
+
187
+ logger.info(`joinClassroom running...
188
+ \tRequest: ${JSON.stringify(req)}`);
189
+
190
+ const cfg: ClassroomConfig = await getClassroomConfig(classID);
191
+
192
+ if (req.registerAs === 'student') {
193
+ if (cfg.students.indexOf(req.user) === -1) {
194
+ cfg.students.push(req.user);
195
+ }
196
+ }
197
+
198
+ await writeClassroomConfig(cfg, classID);
199
+
200
+ const res: JoinClassroom['response'] = {
201
+ ok: true,
202
+ status: Status.ok,
203
+ id_course: classID,
204
+ course_name: cfg.name,
205
+ };
206
+ return res;
207
+ } else {
208
+ return {
209
+ ok: false,
210
+ status: Status.error,
211
+ id_course: '',
212
+ course_name: '',
213
+ errorText: 'No course found with this joincode!',
214
+ };
215
+ }
216
+ }
217
+
218
+ export const ClassroomLeaveQueue = new AsyncProcessQueue<
219
+ // @ts-expect-error Type intersection with username field not properly recognized by AsyncProcessQueue generic
220
+ LeaveClassroom['data'] & { username: string },
221
+ LeaveClassroom['response']
222
+ >(leaveClassroom);
223
+
224
+ export const ClassroomJoinQueue = new AsyncProcessQueue<
225
+ // @ts-expect-error JoinClassroom data type not fully compatible with AsyncProcessQueue generic constraints
226
+ JoinClassroom['data'],
227
+ JoinClassroom['response']
228
+ >(joinClassroom);
229
+
230
+ export const ClassroomCreationQueue = new AsyncProcessQueue<
231
+ // @ts-expect-error CreateClassroom data type not fully compatible with AsyncProcessQueue generic constraints
232
+ CreateClassroom['data'],
233
+ CreateClassroom['response']
234
+ >(createClassroom);