@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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.1.21",
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.21",
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.21"
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
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export * from './core';
2
2
 
3
3
  export { default as CourseLookup } from './impl/couch/courseLookupDB';
4
4
 
5
+ export * from './courseConfigRegistration';
6
+
5
7
  export * from './study';
6
8
 
7
9
  export * from './util';