@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.
- package/.env.development +9 -0
- package/.prettierignore +4 -0
- package/.prettierrc +8 -0
- package/.vscode/launch.json +20 -0
- package/assets/classroomDesignDoc.js +24 -0
- package/assets/courseValidateDocUpdate.js +56 -0
- package/assets/get-tagsDesignDoc.json +9 -0
- package/babel.config.js +6 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +194 -0
- package/dist/app.js.map +1 -0
- package/dist/attachment-preprocessing/index.d.ts +11 -0
- package/dist/attachment-preprocessing/index.d.ts.map +1 -0
- package/dist/attachment-preprocessing/index.js +146 -0
- package/dist/attachment-preprocessing/index.js.map +1 -0
- package/dist/attachment-preprocessing/normalize.d.ts +7 -0
- package/dist/attachment-preprocessing/normalize.d.ts.map +1 -0
- package/dist/attachment-preprocessing/normalize.js +90 -0
- package/dist/attachment-preprocessing/normalize.js.map +1 -0
- package/dist/client-requests/classroom-requests.d.ts +26 -0
- package/dist/client-requests/classroom-requests.d.ts.map +1 -0
- package/dist/client-requests/classroom-requests.js +171 -0
- package/dist/client-requests/classroom-requests.js.map +1 -0
- package/dist/client-requests/course-requests.d.ts +10 -0
- package/dist/client-requests/course-requests.d.ts.map +1 -0
- package/dist/client-requests/course-requests.js +135 -0
- package/dist/client-requests/course-requests.js.map +1 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +70 -0
- package/dist/client.js.map +1 -0
- package/dist/couchdb/authentication.d.ts +4 -0
- package/dist/couchdb/authentication.d.ts.map +1 -0
- package/dist/couchdb/authentication.js +64 -0
- package/dist/couchdb/authentication.js.map +1 -0
- package/dist/couchdb/index.d.ts +18 -0
- package/dist/couchdb/index.d.ts.map +1 -0
- package/dist/couchdb/index.js +52 -0
- package/dist/couchdb/index.js.map +1 -0
- package/dist/design-docs.d.ts +63 -0
- package/dist/design-docs.d.ts.map +1 -0
- package/dist/design-docs.js +90 -0
- package/dist/design-docs.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +62 -0
- package/dist/logger.js.map +1 -0
- package/dist/routes/logs.d.ts +3 -0
- package/dist/routes/logs.d.ts.map +1 -0
- package/dist/routes/logs.js +274 -0
- package/dist/routes/logs.js.map +1 -0
- package/dist/utils/env.d.ts +10 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +38 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/processQueue.d.ts +39 -0
- package/dist/utils/processQueue.d.ts.map +1 -0
- package/dist/utils/processQueue.js +175 -0
- package/dist/utils/processQueue.js.map +1 -0
- package/eslint.config.js +19 -0
- package/jest.config.ts +24 -0
- package/package.json +74 -0
- package/src/app.ts +246 -0
- package/src/attachment-preprocessing/index.ts +204 -0
- package/src/attachment-preprocessing/normalize.ts +123 -0
- package/src/client-requests/classroom-requests.ts +234 -0
- package/src/client-requests/course-requests.ts +188 -0
- package/src/client.ts +97 -0
- package/src/couchdb/authentication.ts +85 -0
- package/src/couchdb/index.ts +76 -0
- package/src/design-docs.ts +107 -0
- package/src/logger.ts +75 -0
- package/src/routes/logs.ts +289 -0
- package/src/utils/env.ts +51 -0
- package/src/utils/processQueue.ts +218 -0
- package/test/client.test.ts +144 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { log } from 'util';
|
|
2
|
+
import { CreateCourse } from '@vue-skuilder/common';
|
|
3
|
+
import CouchDB, { SecurityObject } from '../couchdb/index.js';
|
|
4
|
+
import { postProcessCourse } from '../attachment-preprocessing/index.js';
|
|
5
|
+
import AsyncProcessQueue, { Result } from '../utils/processQueue.js';
|
|
6
|
+
import nano from 'nano';
|
|
7
|
+
|
|
8
|
+
import logger from '../logger.js';
|
|
9
|
+
import { CourseLookup } from '@vue-skuilder/db';
|
|
10
|
+
import { courseDBDesignDocs } from '../design-docs.js';
|
|
11
|
+
|
|
12
|
+
function getCourseDBName(courseID: string): string {
|
|
13
|
+
return `coursedb-${courseID}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Inserts a design document into a course database.
|
|
18
|
+
* @param courseID - The ID of the course database.
|
|
19
|
+
* @param doc
|
|
20
|
+
*/
|
|
21
|
+
function insertDesignDoc(
|
|
22
|
+
courseID: string,
|
|
23
|
+
doc: {
|
|
24
|
+
_id: string;
|
|
25
|
+
}
|
|
26
|
+
): void {
|
|
27
|
+
const courseDB = CouchDB.use(courseID);
|
|
28
|
+
|
|
29
|
+
courseDB
|
|
30
|
+
.get(doc._id)
|
|
31
|
+
.then((priorDoc) => {
|
|
32
|
+
void courseDB.insert({
|
|
33
|
+
...doc,
|
|
34
|
+
_rev: priorDoc._rev,
|
|
35
|
+
});
|
|
36
|
+
})
|
|
37
|
+
.catch(() => {
|
|
38
|
+
void courseDB
|
|
39
|
+
.insert(doc)
|
|
40
|
+
.catch((e) => {
|
|
41
|
+
log(
|
|
42
|
+
`Error inserting design doc ${doc._id} in course-${courseID}: ${e}`
|
|
43
|
+
);
|
|
44
|
+
})
|
|
45
|
+
.then((resp) => {
|
|
46
|
+
if (resp && resp.ok) {
|
|
47
|
+
log(`CourseDB design doc inserted into course-${courseID}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function initCourseDBDesignDocInsert(): Promise<void> {
|
|
54
|
+
const courses = await CourseLookup.allCourses();
|
|
55
|
+
courses.forEach((c) => {
|
|
56
|
+
// Insert design docs
|
|
57
|
+
courseDBDesignDocs.forEach((dd) => {
|
|
58
|
+
insertDesignDoc(getCourseDBName(c._id), dd);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Update security object for public courses
|
|
62
|
+
const courseDB = CouchDB.use<CourseConfig>(getCourseDBName(c._id));
|
|
63
|
+
courseDB
|
|
64
|
+
.get('CourseConfig')
|
|
65
|
+
.then((configDoc) => {
|
|
66
|
+
if (configDoc.public === true) {
|
|
67
|
+
const secObj: SecurityObject = {
|
|
68
|
+
admins: {
|
|
69
|
+
names: [],
|
|
70
|
+
roles: [],
|
|
71
|
+
},
|
|
72
|
+
members: {
|
|
73
|
+
names: [], // Empty array for public courses to allow all users access
|
|
74
|
+
roles: [],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
courseDB
|
|
78
|
+
// @ts-expect-error allow insertion of _security document.
|
|
79
|
+
// db scoped as ConfigDoc to make the read easier.
|
|
80
|
+
.insert(secObj as nano.MaybeDocument, '_security')
|
|
81
|
+
.then(() => {
|
|
82
|
+
logger.info(
|
|
83
|
+
`Updated security settings for public course ${c._id}`
|
|
84
|
+
);
|
|
85
|
+
})
|
|
86
|
+
.catch((e) => {
|
|
87
|
+
logger.error(
|
|
88
|
+
`Error updating security for public course ${c._id}: ${e}`
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.catch((e) => {
|
|
94
|
+
logger.error(`Error getting CourseConfig for ${c._id}: ${e}`);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type CourseConfig = CreateCourse['data'];
|
|
100
|
+
|
|
101
|
+
async function createCourse(cfg: CourseConfig): Promise<Result> {
|
|
102
|
+
cfg.courseID = await CourseLookup.add(cfg.name);
|
|
103
|
+
|
|
104
|
+
if (!cfg.courseID) {
|
|
105
|
+
throw new Error('Course ID not found');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const courseDBName: string = getCourseDBName(cfg.courseID);
|
|
109
|
+
const dbCreation = await CouchDB.db.create(courseDBName);
|
|
110
|
+
|
|
111
|
+
if (dbCreation.ok) {
|
|
112
|
+
const courseDB = CouchDB.use(courseDBName);
|
|
113
|
+
|
|
114
|
+
courseDB
|
|
115
|
+
.insert({
|
|
116
|
+
_id: 'CourseConfig',
|
|
117
|
+
...cfg,
|
|
118
|
+
})
|
|
119
|
+
.catch((e) => {
|
|
120
|
+
logger.error(
|
|
121
|
+
`Error inserting CourseConfig for course ${cfg.courseID}:`,
|
|
122
|
+
e
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// insert the tags, elo, etc view docs
|
|
127
|
+
courseDBDesignDocs.forEach((doc) => {
|
|
128
|
+
courseDB.insert(doc).catch((e) => {
|
|
129
|
+
logger.error(
|
|
130
|
+
`Error inserting design doc for course ${cfg.courseID}:`,
|
|
131
|
+
e
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Configure security for both public and private courses
|
|
137
|
+
const secObj: SecurityObject = {
|
|
138
|
+
admins: {
|
|
139
|
+
names: [],
|
|
140
|
+
roles: [],
|
|
141
|
+
},
|
|
142
|
+
members: {
|
|
143
|
+
names: cfg.public ? [] : [cfg.creator], // Empty array for public courses to allow all users access
|
|
144
|
+
roles: [],
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
courseDB
|
|
149
|
+
.insert(secObj as nano.MaybeDocument, '_security')
|
|
150
|
+
.then(() => {
|
|
151
|
+
logger.info(
|
|
152
|
+
`Successfully set security for ${
|
|
153
|
+
cfg.public ? 'public' : 'private'
|
|
154
|
+
} course ${cfg.courseID}`
|
|
155
|
+
);
|
|
156
|
+
})
|
|
157
|
+
.catch((e) => {
|
|
158
|
+
logger.error(
|
|
159
|
+
`Error inserting security object for course ${cfg.courseID}:`,
|
|
160
|
+
e
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Design documents including validation are inserted via courseDBDesignDocs
|
|
165
|
+
logger.info(
|
|
166
|
+
`Validation design document will be inserted for course ${cfg.courseID}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// follow the course so that user-uploaded content goes through
|
|
171
|
+
// post-processing
|
|
172
|
+
postProcessCourse(cfg.courseID);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
ok: dbCreation.ok ?? false,
|
|
176
|
+
status: 'ok',
|
|
177
|
+
// @ts-expect-error courseID required for runtime but not in Result interface - see sideQuest.md
|
|
178
|
+
courseID: cfg.courseID,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type CreateCourseResp = CreateCourse['response'];
|
|
183
|
+
|
|
184
|
+
export const CourseCreationQueue = new AsyncProcessQueue<
|
|
185
|
+
// @ts-expect-error [I do not know why this thinks is is broken or why it works.]
|
|
186
|
+
CreateCourse['data'],
|
|
187
|
+
CreateCourse['response']
|
|
188
|
+
>(createCourse);
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import axios, { AxiosBasicCredentials, AxiosResponse } from 'axios';
|
|
2
|
+
import {
|
|
3
|
+
AddCourseDataPayload,
|
|
4
|
+
CourseConfig,
|
|
5
|
+
CreateCourse,
|
|
6
|
+
ServerRequestType,
|
|
7
|
+
} from '@vue-skuilder/common';
|
|
8
|
+
import { CreateCourseResp } from './client-requests/course-requests.js';
|
|
9
|
+
|
|
10
|
+
export default class SkldrClient {
|
|
11
|
+
server: string;
|
|
12
|
+
|
|
13
|
+
constructor(server: string) {
|
|
14
|
+
this.server = server;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a new course.
|
|
19
|
+
*/
|
|
20
|
+
async createCourse(
|
|
21
|
+
cfg: CourseConfig,
|
|
22
|
+
auth?: AxiosBasicCredentials
|
|
23
|
+
): Promise<AxiosResponse<CreateCourseResp>> {
|
|
24
|
+
const request: CreateCourse = {
|
|
25
|
+
type: ServerRequestType.CREATE_COURSE,
|
|
26
|
+
data: cfg,
|
|
27
|
+
user: 'apiClient',
|
|
28
|
+
response: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const resp = await axios.post<CreateCourse, AxiosResponse<CreateCourseResp>>(
|
|
32
|
+
`${this.server}`,
|
|
33
|
+
request,
|
|
34
|
+
{
|
|
35
|
+
auth: auth,
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return resp;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a client to interact with the specified course.
|
|
44
|
+
*/
|
|
45
|
+
getCourseClient(id: string): SkldrCourseClient {
|
|
46
|
+
return new SkldrCourseClient(this.server, id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @returns a list of all courses on the server in `id - name` format.
|
|
51
|
+
*/
|
|
52
|
+
async getCourses(): Promise<string[]> {
|
|
53
|
+
const resp = await axios.get(`${this.server}/courses`);
|
|
54
|
+
return resp.data;
|
|
55
|
+
}
|
|
56
|
+
async getVersion(): Promise<string> {
|
|
57
|
+
const resp = await axios.get(`${this.server}/version`);
|
|
58
|
+
return resp.data;
|
|
59
|
+
}
|
|
60
|
+
async getRoles(): Promise<string[]> {
|
|
61
|
+
const resp = await axios.get(`${this.server}/roles`);
|
|
62
|
+
return resp.data;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class SkldrCourseClient {
|
|
67
|
+
id: string;
|
|
68
|
+
server: string;
|
|
69
|
+
|
|
70
|
+
constructor(server: string, id: string) {
|
|
71
|
+
this.server = server;
|
|
72
|
+
this.id = id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
addData(data: AddCourseDataPayload): Promise<Express.Response> {
|
|
76
|
+
return axios.post(`${this.server}/${this.id}`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: data,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async deleteCourse(auth?: AxiosBasicCredentials): Promise<Express.Response> {
|
|
83
|
+
// [ ] Consider auth / permanence of "destructive" actions
|
|
84
|
+
return axios.delete(`${this.server}/course/${this.id}`, {
|
|
85
|
+
auth,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// [ ] remove also from the `coursedb-lookup` database (?)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getConfig(): Promise<CourseConfig> {
|
|
92
|
+
const resp = await axios.get<void, AxiosResponse<CourseConfig>>(
|
|
93
|
+
`${this.server}/course/${this.id}/config`
|
|
94
|
+
);
|
|
95
|
+
return resp.data;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Nano from 'nano';
|
|
2
|
+
import { COUCH_URL_WITH_PROTOCOL } from './index.js';
|
|
3
|
+
import { VueClientRequest } from '../app.js';
|
|
4
|
+
import logger from '../logger.js';
|
|
5
|
+
|
|
6
|
+
interface CouchSession {
|
|
7
|
+
info: {
|
|
8
|
+
authenticated: string;
|
|
9
|
+
authentication_db: string;
|
|
10
|
+
authentication_handlers: string[];
|
|
11
|
+
};
|
|
12
|
+
ok: boolean;
|
|
13
|
+
userCtx: {
|
|
14
|
+
name: string;
|
|
15
|
+
roles: string[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function requestIsAdminAuthenticated(req: VueClientRequest) {
|
|
20
|
+
logRequest(req);
|
|
21
|
+
|
|
22
|
+
const username = req.body.user;
|
|
23
|
+
const authCookie: string = req.cookies.AuthSession ? req.cookies.AuthSession : 'null';
|
|
24
|
+
|
|
25
|
+
if (authCookie === 'null') {
|
|
26
|
+
return false;
|
|
27
|
+
} else {
|
|
28
|
+
return await Nano({
|
|
29
|
+
cookie: 'AuthSession=' + authCookie,
|
|
30
|
+
url: COUCH_URL_WITH_PROTOCOL,
|
|
31
|
+
})
|
|
32
|
+
.session()
|
|
33
|
+
.then((s: CouchSession) => {
|
|
34
|
+
logger.info(`AuthUser: ${JSON.stringify(s)}`);
|
|
35
|
+
const isAdmin = s.userCtx.roles.indexOf('_admin') !== -1;
|
|
36
|
+
const isLoggedInUser = s.userCtx.name === username;
|
|
37
|
+
return isAdmin && isLoggedInUser;
|
|
38
|
+
})
|
|
39
|
+
.catch((_err) => {
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function logRequest(req: VueClientRequest) {
|
|
46
|
+
logger.info(`${req.body.type} request from ${req.body.user}...`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function requestIsAuthenticated(req: VueClientRequest) {
|
|
50
|
+
logRequest(req);
|
|
51
|
+
|
|
52
|
+
if (req.headers.authorization) {
|
|
53
|
+
const auth = Buffer.from(req.headers.authorization.split(' ')[1], 'base64')
|
|
54
|
+
.toString('ascii')
|
|
55
|
+
.split(':');
|
|
56
|
+
const username = auth[0];
|
|
57
|
+
const password = auth[1];
|
|
58
|
+
|
|
59
|
+
const authResult = await Nano({
|
|
60
|
+
url: COUCH_URL_WITH_PROTOCOL,
|
|
61
|
+
}).auth(username, password);
|
|
62
|
+
|
|
63
|
+
return authResult.ok;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const username = req.body.user;
|
|
67
|
+
const authCookie: string = req.cookies.AuthSession ? req.cookies.AuthSession : 'null';
|
|
68
|
+
|
|
69
|
+
if (authCookie === 'null') {
|
|
70
|
+
return false;
|
|
71
|
+
} else {
|
|
72
|
+
return await Nano({
|
|
73
|
+
cookie: 'AuthSession=' + authCookie,
|
|
74
|
+
url: COUCH_URL_WITH_PROTOCOL,
|
|
75
|
+
})
|
|
76
|
+
.session()
|
|
77
|
+
.then((s: CouchSession) => {
|
|
78
|
+
logger.info(`AuthUser: ${JSON.stringify(s)}`);
|
|
79
|
+
return s.userCtx.name === username;
|
|
80
|
+
})
|
|
81
|
+
.catch((_err) => {
|
|
82
|
+
return false;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Nano from 'nano';
|
|
2
|
+
import process from 'process';
|
|
3
|
+
import ENV from '../utils/env.js';
|
|
4
|
+
import logger from '../logger.js';
|
|
5
|
+
|
|
6
|
+
const url = ENV.COUCHDB_SERVER;
|
|
7
|
+
const protocol: string = ENV.COUCHDB_PROTOCOL;
|
|
8
|
+
|
|
9
|
+
const admin = {
|
|
10
|
+
username: ENV.COUCHDB_ADMIN,
|
|
11
|
+
password: ENV.COUCHDB_PASSWORD,
|
|
12
|
+
};
|
|
13
|
+
const credentialCouchURL = `${protocol}://${admin.username}:${admin.password}@${url}`;
|
|
14
|
+
|
|
15
|
+
logger.info('WORKING DIRECTORY: ' + process.cwd());
|
|
16
|
+
logger.info(
|
|
17
|
+
`CouchDB url: ${url}
|
|
18
|
+
protocol: ${protocol}
|
|
19
|
+
credentials:
|
|
20
|
+
\tusername: ${admin.username}
|
|
21
|
+
\tpassword: *****
|
|
22
|
+
credUrl: ${protocol}://${admin.username}:*****@${url}
|
|
23
|
+
`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const CouchDB = Nano(credentialCouchURL);
|
|
27
|
+
|
|
28
|
+
export async function useOrCreateCourseDB(courseID: string): Promise<Nano.DocumentScope<unknown>> {
|
|
29
|
+
return useOrCreateDB(`coursedb-${courseID}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface NanoError extends Error {
|
|
33
|
+
statusCode?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function useOrCreateDB<T>(dbName: string): Promise<Nano.DocumentScope<T>> {
|
|
37
|
+
const ret = CouchDB.use<T>(dbName);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await ret.info();
|
|
41
|
+
return ret;
|
|
42
|
+
} catch {
|
|
43
|
+
try {
|
|
44
|
+
await CouchDB.db.create(dbName);
|
|
45
|
+
return ret;
|
|
46
|
+
} catch (error: unknown) {
|
|
47
|
+
const createErr = error as NanoError;
|
|
48
|
+
// If error is "database already exists", return existing db
|
|
49
|
+
if (createErr.statusCode === 412) {
|
|
50
|
+
return ret;
|
|
51
|
+
}
|
|
52
|
+
throw createErr;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function docCount(dbName: string): Promise<number> {
|
|
58
|
+
const db = await useOrCreateDB(dbName);
|
|
59
|
+
const info = await db.info();
|
|
60
|
+
return info.doc_count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SecurityObject extends Nano.MaybeDocument {
|
|
64
|
+
admins: {
|
|
65
|
+
names: string[];
|
|
66
|
+
roles: string[];
|
|
67
|
+
};
|
|
68
|
+
members: {
|
|
69
|
+
names: string[];
|
|
70
|
+
roles: string[];
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const COUCH_URL_WITH_PROTOCOL = protocol + '://' + process.env.COUCHDB_SERVER;
|
|
75
|
+
|
|
76
|
+
export default CouchDB;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CardData, DocType } from '@vue-skuilder/db';
|
|
2
|
+
import * as fileSystem from 'fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fake fcn to allow usage in couchdb map fcns which, after passing
|
|
6
|
+
* through `.toString()`, are applied to all courses
|
|
7
|
+
*/
|
|
8
|
+
function emit(key?: unknown, value?: unknown): [unknown, unknown] {
|
|
9
|
+
return [key, value];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Load design documents
|
|
13
|
+
export const classroomDbDesignDoc = fileSystem.readFileSync(
|
|
14
|
+
'./assets/classroomDesignDoc.js',
|
|
15
|
+
'utf-8'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export const courseDBDesignDoc = fileSystem.readFileSync(
|
|
19
|
+
'./assets/get-tagsDesignDoc.json',
|
|
20
|
+
'utf-8'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const courseValidateDocUpdate = fileSystem.readFileSync(
|
|
24
|
+
'./assets/courseValidateDocUpdate.js',
|
|
25
|
+
'utf-8'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const elodoc = {
|
|
29
|
+
_id: '_design/elo',
|
|
30
|
+
views: {
|
|
31
|
+
elo: {
|
|
32
|
+
map: `function (doc) {
|
|
33
|
+
if (doc.docType && doc.docType === 'CARD') {
|
|
34
|
+
if (doc.elo && typeof(doc.elo) === 'number') {
|
|
35
|
+
emit(doc.elo, doc._id);
|
|
36
|
+
} else if (doc.elo && doc.elo.global) {
|
|
37
|
+
emit(doc.elo.global.score, doc._id);
|
|
38
|
+
} else if (doc.elo) {
|
|
39
|
+
emit(doc.elo.score, doc._id);
|
|
40
|
+
} else {
|
|
41
|
+
const randElo = 995 + Math.round(10 * Math.random());
|
|
42
|
+
emit(randElo, doc._id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}`,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
language: 'javascript',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const tagsDoc = {
|
|
52
|
+
_id: '_design/getTags',
|
|
53
|
+
views: {
|
|
54
|
+
getTags: {
|
|
55
|
+
map: `function (doc) {
|
|
56
|
+
if (doc.docType && doc.docType === "TAG") {
|
|
57
|
+
for (var cardIndex in doc.taggedCards) {
|
|
58
|
+
emit(doc.taggedCards[cardIndex], {
|
|
59
|
+
docType: doc.docType,
|
|
60
|
+
name: doc.name,
|
|
61
|
+
snippit: doc.snippit,
|
|
62
|
+
wiki: '',
|
|
63
|
+
taggedCards: []
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}`,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
language: 'javascript',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const cardsByInexperienceDoc = {
|
|
74
|
+
_id: '_design/cardsByInexperience',
|
|
75
|
+
views: {
|
|
76
|
+
cardsByInexperience: {
|
|
77
|
+
map: function (doc: CardData) {
|
|
78
|
+
if (doc.docType && doc.docType === DocType.CARD) {
|
|
79
|
+
if (
|
|
80
|
+
doc.elo &&
|
|
81
|
+
doc.elo.global &&
|
|
82
|
+
typeof doc.elo.global.count == 'number'
|
|
83
|
+
) {
|
|
84
|
+
emit(doc.elo.global.count, doc.elo);
|
|
85
|
+
} else if (doc.elo && typeof doc.elo == 'number') {
|
|
86
|
+
emit(0, doc.elo);
|
|
87
|
+
} else {
|
|
88
|
+
emit(0, 995 + Math.floor(10 * Math.random()));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}.toString(),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
language: 'javascript',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const authDesignDoc = {
|
|
98
|
+
_id: '_design/_auth',
|
|
99
|
+
validate_doc_update: courseValidateDocUpdate,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const courseDBDesignDocs = [
|
|
103
|
+
elodoc,
|
|
104
|
+
tagsDoc,
|
|
105
|
+
cardsByInexperienceDoc,
|
|
106
|
+
authDesignDoc,
|
|
107
|
+
];
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createLogger, format, transports } from 'winston';
|
|
2
|
+
import DailyRotateFile from 'winston-daily-rotate-file';
|
|
3
|
+
|
|
4
|
+
const callerFormat = format((info) => {
|
|
5
|
+
const error = new Error();
|
|
6
|
+
const stackLines = error.stack?.split('\n');
|
|
7
|
+
|
|
8
|
+
if (stackLines) {
|
|
9
|
+
// Find the first line that isn't from node_modules or internal Winston code
|
|
10
|
+
const callerLine = stackLines.find((line) => {
|
|
11
|
+
return (
|
|
12
|
+
line.includes('(') &&
|
|
13
|
+
!line.includes('node_modules') &&
|
|
14
|
+
!line.includes('logger.ts') &&
|
|
15
|
+
!line.includes('winston')
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (callerLine) {
|
|
20
|
+
// Try to extract function name and location
|
|
21
|
+
const match = callerLine.match(
|
|
22
|
+
/at (?:(?:Object|Module)\.)?([a-zA-Z0-9_$.]+)? \((.+?):(\d+):(\d+)\)/
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (match) {
|
|
26
|
+
const [, functionName = '<anonymous>', file, line] = match;
|
|
27
|
+
// Clean up the file path - get just filename
|
|
28
|
+
const fileName = file.split('/').pop() || file;
|
|
29
|
+
info.caller = `${functionName} (${fileName}:${line})`;
|
|
30
|
+
} else {
|
|
31
|
+
// Handle edge cases (like anonymous functions)
|
|
32
|
+
const basicMatch = callerLine.match(/\((.+?):(\d+):(\d+)\)$/);
|
|
33
|
+
if (basicMatch) {
|
|
34
|
+
const [, file, line] = basicMatch;
|
|
35
|
+
const fileName = file.split('/').pop() || file;
|
|
36
|
+
info.caller = `<anonymous> (${fileName}:${line})`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return info;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const logger = createLogger({
|
|
46
|
+
level: 'info',
|
|
47
|
+
format: format.combine(callerFormat(), format.timestamp(), format.json()),
|
|
48
|
+
transports: [
|
|
49
|
+
new transports.Console({
|
|
50
|
+
format: format.combine(
|
|
51
|
+
callerFormat(),
|
|
52
|
+
format.timestamp(),
|
|
53
|
+
format.printf(({ level, message, timestamp, caller, ...rest }) => {
|
|
54
|
+
const meta = Object.keys(rest).length ? JSON.stringify(rest) : '';
|
|
55
|
+
return `${timestamp} [${level}] ${caller || 'unknown'} - ${message} ${meta}`;
|
|
56
|
+
})
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
new DailyRotateFile({
|
|
60
|
+
filename: 'logs/error-%DATE%.log',
|
|
61
|
+
datePattern: 'YYYY-MM-DD',
|
|
62
|
+
level: 'error',
|
|
63
|
+
maxFiles: '7d',
|
|
64
|
+
maxSize: '20m',
|
|
65
|
+
}),
|
|
66
|
+
new DailyRotateFile({
|
|
67
|
+
filename: 'logs/combined-%DATE%.log',
|
|
68
|
+
datePattern: 'YYYY-MM-DD',
|
|
69
|
+
maxFiles: '7d',
|
|
70
|
+
maxSize: '20m',
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export default logger;
|