@vue-skuilder/db 0.1.21 → 0.1.23
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/dist/index.d.cts +117 -4
- package/dist/index.d.ts +117 -4
- package/dist/index.js +270 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/courseConfigRegistration.ts +439 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.1.
|
|
7
|
+
"version": "0.1.23",
|
|
8
8
|
"description": "Database layer for vue-skuilder",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
51
|
-
"@vue-skuilder/common": "0.1.
|
|
51
|
+
"@vue-skuilder/common": "0.1.23",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"vite": "^7.0.0",
|
|
63
63
|
"vitest": "^4.0.15"
|
|
64
64
|
},
|
|
65
|
-
"stableVersion": "0.1.
|
|
65
|
+
"stableVersion": "0.1.23"
|
|
66
66
|
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { CourseConfig, DataShape, NameSpacer, toZodJSON } from '@vue-skuilder/common';
|
|
2
|
+
import { CourseDBInterface } from './core/interfaces/courseDB.js';
|
|
3
|
+
import { logger } from './util/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for custom questions data structure returned by allCustomQuestions()
|
|
7
|
+
*/
|
|
8
|
+
export interface CustomQuestionsData {
|
|
9
|
+
courses: { name: string }[]; // Course instances with question instances
|
|
10
|
+
questionClasses: {
|
|
11
|
+
name: string;
|
|
12
|
+
dataShapes?: DataShape[];
|
|
13
|
+
views?: { name?: string }[];
|
|
14
|
+
seedData?: unknown[];
|
|
15
|
+
}[]; // Question class constructors
|
|
16
|
+
dataShapes: DataShape[]; // DataShape definitions for studio-ui
|
|
17
|
+
views: { name?: string }[]; // Vue components for rendering
|
|
18
|
+
meta: {
|
|
19
|
+
questionCount: number;
|
|
20
|
+
dataShapeCount: number;
|
|
21
|
+
viewCount: number;
|
|
22
|
+
courseCount: number;
|
|
23
|
+
packageName: string;
|
|
24
|
+
sourceDirectory: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Interface for processed question data for registration
|
|
30
|
+
*/
|
|
31
|
+
export interface ProcessedQuestionData {
|
|
32
|
+
name: string;
|
|
33
|
+
course: string;
|
|
34
|
+
questionClass: {
|
|
35
|
+
name: string;
|
|
36
|
+
dataShapes?: DataShape[];
|
|
37
|
+
views?: { name?: string }[];
|
|
38
|
+
seedData?: unknown[];
|
|
39
|
+
};
|
|
40
|
+
dataShapes: DataShape[];
|
|
41
|
+
views: { name?: string }[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Interface for processed data shape for registration
|
|
46
|
+
*/
|
|
47
|
+
export interface ProcessedDataShape {
|
|
48
|
+
name: string;
|
|
49
|
+
course: string;
|
|
50
|
+
dataShape: DataShape;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a data shape is already registered in the course config with valid schema
|
|
55
|
+
*/
|
|
56
|
+
export function isDataShapeRegistered(
|
|
57
|
+
dataShape: ProcessedDataShape,
|
|
58
|
+
courseConfig: CourseConfig
|
|
59
|
+
): boolean {
|
|
60
|
+
const namespacedName = NameSpacer.getDataShapeString({
|
|
61
|
+
dataShape: dataShape.name,
|
|
62
|
+
course: dataShape.course,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const existingDataShape = courseConfig.dataShapes.find((ds) => ds.name === namespacedName);
|
|
66
|
+
|
|
67
|
+
// existence sufficient to be considered "registered"
|
|
68
|
+
return existingDataShape !== undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isDataShapeSchemaAvailable(
|
|
72
|
+
dataShape: ProcessedDataShape,
|
|
73
|
+
courseConfig: CourseConfig
|
|
74
|
+
): boolean {
|
|
75
|
+
const namespacedName = NameSpacer.getDataShapeString({
|
|
76
|
+
dataShape: dataShape.name,
|
|
77
|
+
course: dataShape.course,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const existingDataShape = courseConfig.dataShapes.find((ds) => ds.name === namespacedName);
|
|
81
|
+
|
|
82
|
+
return existingDataShape !== undefined && existingDataShape.serializedZodSchema !== undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a question type is already registered in the course config
|
|
87
|
+
*/
|
|
88
|
+
export function isQuestionTypeRegistered(
|
|
89
|
+
question: ProcessedQuestionData,
|
|
90
|
+
courseConfig: CourseConfig
|
|
91
|
+
): boolean {
|
|
92
|
+
const namespacedName = NameSpacer.getQuestionString({
|
|
93
|
+
course: question.course,
|
|
94
|
+
questionType: question.name,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return courseConfig.questionTypes.some((qt) => qt.name === namespacedName);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Process custom questions data into registration-ready format
|
|
102
|
+
*/
|
|
103
|
+
export function processCustomQuestionsData(customQuestions: CustomQuestionsData): {
|
|
104
|
+
dataShapes: ProcessedDataShape[];
|
|
105
|
+
questions: ProcessedQuestionData[];
|
|
106
|
+
} {
|
|
107
|
+
const processedDataShapes: ProcessedDataShape[] = [];
|
|
108
|
+
const processedQuestions: ProcessedQuestionData[] = [];
|
|
109
|
+
|
|
110
|
+
// Extract course names from the custom questions
|
|
111
|
+
const courseNames = customQuestions.courses.map((course) => course.name);
|
|
112
|
+
|
|
113
|
+
// Process each question class
|
|
114
|
+
customQuestions.questionClasses.forEach((questionClass) => {
|
|
115
|
+
// Determine the course name (use first course or default to meta.packageName)
|
|
116
|
+
const courseName = courseNames.length > 0 ? courseNames[0] : customQuestions.meta.packageName;
|
|
117
|
+
|
|
118
|
+
// Process data shapes from this question class (static property)
|
|
119
|
+
if (questionClass.dataShapes && Array.isArray(questionClass.dataShapes)) {
|
|
120
|
+
questionClass.dataShapes.forEach((dataShape) => {
|
|
121
|
+
processedDataShapes.push({
|
|
122
|
+
name: dataShape.name,
|
|
123
|
+
course: courseName,
|
|
124
|
+
dataShape: dataShape,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Process the question itself
|
|
130
|
+
processedQuestions.push({
|
|
131
|
+
name: questionClass.name,
|
|
132
|
+
course: courseName,
|
|
133
|
+
questionClass: questionClass,
|
|
134
|
+
dataShapes: questionClass.dataShapes || [],
|
|
135
|
+
views: questionClass.views || [],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return { dataShapes: processedDataShapes, questions: processedQuestions };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Register a data shape in the course config
|
|
144
|
+
*/
|
|
145
|
+
export function registerDataShape(
|
|
146
|
+
dataShape: ProcessedDataShape,
|
|
147
|
+
courseConfig: CourseConfig
|
|
148
|
+
): boolean {
|
|
149
|
+
const namespacedName = NameSpacer.getDataShapeString({
|
|
150
|
+
dataShape: dataShape.name,
|
|
151
|
+
course: dataShape.course,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Generate JSON Schema for the DataShape
|
|
155
|
+
let serializedZodSchema: string | undefined;
|
|
156
|
+
try {
|
|
157
|
+
serializedZodSchema = toZodJSON(dataShape.dataShape);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.warn(`Failed to generate schema for ${namespacedName}:`, error);
|
|
160
|
+
serializedZodSchema = undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if DataShape already exists
|
|
164
|
+
const existingIndex = courseConfig.dataShapes.findIndex((ds) => ds.name === namespacedName);
|
|
165
|
+
|
|
166
|
+
if (existingIndex !== -1) {
|
|
167
|
+
const existingDataShape = courseConfig.dataShapes[existingIndex];
|
|
168
|
+
|
|
169
|
+
// If existing schema matches new schema, no update needed
|
|
170
|
+
if (existingDataShape.serializedZodSchema === serializedZodSchema) {
|
|
171
|
+
logger.info(
|
|
172
|
+
`DataShape '${dataShape.name}' from '${dataShape.course}' already registered with identical schema`
|
|
173
|
+
);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Schema has changed or was missing - update it
|
|
178
|
+
if (existingDataShape.serializedZodSchema) {
|
|
179
|
+
logger.info(
|
|
180
|
+
`DataShape '${dataShape.name}' from '${dataShape.course}' schema has changed, updating...`
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
logger.info(
|
|
184
|
+
`DataShape '${dataShape.name}' from '${dataShape.course}' already registered, but with no schema. Adding schema to existing entry`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Update the existing entry
|
|
189
|
+
courseConfig.dataShapes[existingIndex] = {
|
|
190
|
+
name: namespacedName,
|
|
191
|
+
questionTypes: existingDataShape.questionTypes, // Preserve existing question type associations
|
|
192
|
+
serializedZodSchema,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
logger.info(`Updated DataShape: ${namespacedName}`);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
courseConfig.dataShapes.push({
|
|
200
|
+
name: namespacedName,
|
|
201
|
+
questionTypes: [],
|
|
202
|
+
serializedZodSchema,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
logger.info(`Registered DataShape: ${namespacedName}`);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Register a question type in the course config
|
|
211
|
+
*/
|
|
212
|
+
export function registerQuestionType(
|
|
213
|
+
question: ProcessedQuestionData,
|
|
214
|
+
courseConfig: CourseConfig
|
|
215
|
+
): boolean {
|
|
216
|
+
if (isQuestionTypeRegistered(question, courseConfig)) {
|
|
217
|
+
logger.info(`QuestionType '${question.name}' from '${question.course}' already registered`);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const namespacedQuestionName = NameSpacer.getQuestionString({
|
|
222
|
+
course: question.course,
|
|
223
|
+
questionType: question.name,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Build view list
|
|
227
|
+
const viewList = question.views.map((view) => {
|
|
228
|
+
if (view.name) {
|
|
229
|
+
return view.name;
|
|
230
|
+
} else {
|
|
231
|
+
return 'unnamedComponent';
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Build data shape list
|
|
236
|
+
const dataShapeList = question.dataShapes.map((dataShape) =>
|
|
237
|
+
NameSpacer.getDataShapeString({
|
|
238
|
+
course: question.course,
|
|
239
|
+
dataShape: dataShape.name,
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Add question type to course config
|
|
244
|
+
courseConfig.questionTypes.push({
|
|
245
|
+
name: namespacedQuestionName,
|
|
246
|
+
viewList: viewList,
|
|
247
|
+
dataShapeList: dataShapeList,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Cross-reference: Add this question type to its data shapes
|
|
251
|
+
question.dataShapes.forEach((dataShape) => {
|
|
252
|
+
const namespacedDataShapeName = NameSpacer.getDataShapeString({
|
|
253
|
+
course: question.course,
|
|
254
|
+
dataShape: dataShape.name,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
for (const ds of courseConfig.dataShapes) {
|
|
258
|
+
if (ds.name === namespacedDataShapeName) {
|
|
259
|
+
ds.questionTypes.push(namespacedQuestionName);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
logger.info(`Registered QuestionType: ${namespacedQuestionName}`);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Register seed data for a question type
|
|
270
|
+
*
|
|
271
|
+
* @param question - The processed question data
|
|
272
|
+
* @param courseDB - The course database interface
|
|
273
|
+
* @param username - The username to attribute seed data to
|
|
274
|
+
*/
|
|
275
|
+
export async function registerSeedData(
|
|
276
|
+
question: ProcessedQuestionData,
|
|
277
|
+
courseDB: CourseDBInterface,
|
|
278
|
+
username: string
|
|
279
|
+
): Promise<void> {
|
|
280
|
+
if (question.questionClass.seedData && Array.isArray(question.questionClass.seedData)) {
|
|
281
|
+
logger.info(`Registering seed data for question: ${question.name}`);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const seedDataPromises = question.questionClass.seedData
|
|
285
|
+
.filter(() => question.dataShapes.length > 0)
|
|
286
|
+
.map((seedDataItem: unknown) =>
|
|
287
|
+
courseDB.addNote(
|
|
288
|
+
question.course,
|
|
289
|
+
question.dataShapes[0],
|
|
290
|
+
seedDataItem,
|
|
291
|
+
username,
|
|
292
|
+
[]
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
await Promise.all(seedDataPromises);
|
|
297
|
+
logger.info(`Seed data registered for question: ${question.name}`);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger.warn(
|
|
300
|
+
`Failed to register seed data for question '${question.name}': ${error instanceof Error ? error.message : String(error)}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Register BlanksCard (markdown fillIn) question type specifically
|
|
308
|
+
*/
|
|
309
|
+
export async function registerBlanksCard(
|
|
310
|
+
BlanksCard: { name: string; views?: { name?: string }[] },
|
|
311
|
+
BlanksCardDataShapes: DataShape[],
|
|
312
|
+
courseConfig: CourseConfig,
|
|
313
|
+
courseDB: CourseDBInterface,
|
|
314
|
+
username?: string
|
|
315
|
+
): Promise<{ success: boolean; errorMessage?: string }> {
|
|
316
|
+
try {
|
|
317
|
+
logger.info('Registering BlanksCard data shapes and question type...');
|
|
318
|
+
|
|
319
|
+
let registeredCount = 0;
|
|
320
|
+
const courseName = 'default'; // BlanksCard comes from the default course
|
|
321
|
+
|
|
322
|
+
// Register BlanksCard data shapes
|
|
323
|
+
for (const dataShapeClass of BlanksCardDataShapes) {
|
|
324
|
+
const processedDataShape: ProcessedDataShape = {
|
|
325
|
+
name: dataShapeClass.name,
|
|
326
|
+
course: courseName,
|
|
327
|
+
dataShape: dataShapeClass,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (registerDataShape(processedDataShape, courseConfig)) {
|
|
331
|
+
registeredCount++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Register BlanksCard question type
|
|
336
|
+
const processedQuestion: ProcessedQuestionData = {
|
|
337
|
+
name: BlanksCard.name,
|
|
338
|
+
course: courseName,
|
|
339
|
+
questionClass: {
|
|
340
|
+
name: BlanksCard.name,
|
|
341
|
+
dataShapes: BlanksCardDataShapes,
|
|
342
|
+
views: BlanksCard.views || [],
|
|
343
|
+
},
|
|
344
|
+
dataShapes: BlanksCardDataShapes,
|
|
345
|
+
views: BlanksCard.views || [],
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
if (registerQuestionType(processedQuestion, courseConfig)) {
|
|
349
|
+
registeredCount++;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Update the course config in the database
|
|
353
|
+
logger.info('Updating course configuration with BlanksCard...');
|
|
354
|
+
const updateResult = await courseDB.updateCourseConfig(courseConfig);
|
|
355
|
+
|
|
356
|
+
if (!updateResult.ok) {
|
|
357
|
+
throw new Error(`Failed to update course config: ${JSON.stringify(updateResult)}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Register seed data if BlanksCard has any and username provided
|
|
361
|
+
if (username) {
|
|
362
|
+
await registerSeedData(processedQuestion, courseDB, username);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
logger.info(`BlanksCard registration complete: ${registeredCount} items registered`);
|
|
366
|
+
|
|
367
|
+
return { success: true };
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
370
|
+
logger.error(`BlanksCard registration failed: ${errorMessage}`);
|
|
371
|
+
return { success: false, errorMessage };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Main function to register all custom question types and data shapes
|
|
377
|
+
*
|
|
378
|
+
* @param customQuestions - The custom questions data from allCustomQuestions()
|
|
379
|
+
* @param courseConfig - The course configuration object
|
|
380
|
+
* @param courseDB - The course database interface
|
|
381
|
+
* @param username - The username to attribute seed data to
|
|
382
|
+
*/
|
|
383
|
+
export async function registerCustomQuestionTypes(
|
|
384
|
+
customQuestions: CustomQuestionsData,
|
|
385
|
+
courseConfig: CourseConfig,
|
|
386
|
+
courseDB: CourseDBInterface,
|
|
387
|
+
username?: string
|
|
388
|
+
): Promise<{ success: boolean; registeredCount: number; errorMessage?: string }> {
|
|
389
|
+
try {
|
|
390
|
+
logger.info('Beginning custom question registration');
|
|
391
|
+
logger.info(`Processing ${customQuestions.questionClasses.length} question classes`);
|
|
392
|
+
|
|
393
|
+
const { dataShapes, questions } = processCustomQuestionsData(customQuestions);
|
|
394
|
+
|
|
395
|
+
logger.info(`Found ${dataShapes.length} data shapes and ${questions.length} questions`);
|
|
396
|
+
|
|
397
|
+
let registeredCount = 0;
|
|
398
|
+
|
|
399
|
+
// First, register all data shapes
|
|
400
|
+
logger.info('Registering data shapes...');
|
|
401
|
+
for (const dataShape of dataShapes) {
|
|
402
|
+
if (registerDataShape(dataShape, courseConfig)) {
|
|
403
|
+
registeredCount++;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Then, register all question types
|
|
408
|
+
logger.info('Registering question types...');
|
|
409
|
+
for (const question of questions) {
|
|
410
|
+
if (registerQuestionType(question, courseConfig)) {
|
|
411
|
+
registeredCount++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Update the course config in the database
|
|
416
|
+
logger.info('Updating course configuration...');
|
|
417
|
+
const updateResult = await courseDB.updateCourseConfig(courseConfig);
|
|
418
|
+
|
|
419
|
+
if (!updateResult.ok) {
|
|
420
|
+
throw new Error(`Failed to update course config: ${JSON.stringify(updateResult)}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Register seed data for questions that have it (if username provided)
|
|
424
|
+
if (username) {
|
|
425
|
+
logger.info('Registering seed data...');
|
|
426
|
+
for (const question of questions) {
|
|
427
|
+
await registerSeedData(question, courseDB, username);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
logger.info(`Custom question registration complete: ${registeredCount} items registered`);
|
|
432
|
+
|
|
433
|
+
return { success: true, registeredCount };
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
436
|
+
logger.error(`Custom question registration failed: ${errorMessage}`);
|
|
437
|
+
return { success: false, registeredCount: 0, errorMessage };
|
|
438
|
+
}
|
|
439
|
+
}
|