@loka-sms/core-integration-be 0.0.1 → 0.0.3

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,6 +1,6 @@
1
- # @loka-sms/core-integration-be
1
+ # @loka-sms/core-integration-be
2
2
 
3
- NestJS helper package untuk backend modul Loka SMS yang perlu mengakses data Core via service-to-service auth (`X-App-ID` + `X-App-Secret`).
3
+ NestJS helper package untuk backend modul OMNISCHOOL yang perlu mengakses data Core via service-to-service auth (`X-App-ID` + `X-App-Secret`).
4
4
 
5
5
  Package ini cocok untuk **worker/background job/service sync**, bukan untuk forwarding token user.
6
6
 
@@ -28,9 +28,9 @@ npm install @loka-sms/core-integration-be
28
28
 
29
29
  | Variabel | Wajib | Fallback | Keterangan |
30
30
  |---|---|---|---|
31
- | `CORE_SERVICE_URL` | | | Base URL Gateway, contoh: `http://localhost:3000/api` |
32
- | `CORE_APP_ID` | | `APP_ID` | Application ID yang terdaftar di Core |
33
- | `CORE_APP_SECRET` | | `APP_SECRET` | Application secret (plain, bukan bcrypt) |
31
+ | `CORE_SERVICE_URL` | ✅ | — | Base URL Gateway, contoh: `http://localhost:3000/api` |
32
+ | `CORE_APP_ID` | ✅ | `APP_ID` | Application ID yang terdaftar di Core |
33
+ | `CORE_APP_SECRET` | ✅ | `APP_SECRET` | Application secret (plain, bukan bcrypt) |
34
34
 
35
35
  Contoh `.env`:
36
36
 
@@ -89,7 +89,7 @@ export class RaporSyncService {
89
89
 
90
90
  async generate(schoolId: string) {
91
91
  const students = await this.core.getStudents(schoolId, { status: 'active' });
92
- const classes = await this.core.getClasses(schoolId);
92
+ const classes = await this.core.getClasses(schoolId, { grade: 7 });
93
93
  return { students, classes };
94
94
  }
95
95
  }
@@ -107,10 +107,12 @@ Package selalu mengirim `X-App-ID`, `X-App-Secret`, dan `X-School-ID`. Tidak per
107
107
  | `findStudentByEmail(email, schoolId)` | `GET /api/students?q=...&limit=1` |
108
108
  | `getParentChildrenByUserId(parentUserId, schoolId)` | `GET /api/parents/by-user/:id/children` |
109
109
  | `getParentChildIdsByUserId(parentUserId, schoolId)` | `GET /api/parents/by-user/:id/children` |
110
+ | `getParentChildUserIdsByUserId(parentUserId, schoolId)` | `GET /api/parents/by-user/:id/children` |
110
111
  | `resolveParentStudentScope(parentUserId, schoolId, requestedStudentId?)` | `GET /api/parents/by-user/:id/children` |
112
+ | `resolveParentStudentUserScope(parentUserId, schoolId, requestedStudentUserId?)` | `GET /api/parents/by-user/:id/children` |
111
113
  | `getTeachers(schoolId)` | `GET /api/teachers` |
112
114
  | `getTeacher(teacherId, schoolId)` | `GET /api/teachers/:id` |
113
- | `getClasses(schoolId)` | `GET /api/classes` |
115
+ | `getClasses(schoolId, params)` | `GET /api/classes` |
114
116
  | `getSubjects(schoolId, params)` | `GET /api/subjects` |
115
117
  | `getCurriculums(schoolId, params)` | `GET /api/curriculum` |
116
118
  | `getAcademicCalendar(schoolId, params)` | `GET /api/academic-calendar` |
@@ -138,6 +140,12 @@ await this.core.post('notifications', schoolId, {
138
140
 
139
141
  Path tidak perlu diawali `/api` atau `/api/v1`.
140
142
 
143
+ ## Catatan Identity Siswa
144
+
145
+ - `getParentChildIdsByUserId()` dan `resolveParentStudentScope()` mengembalikan **Core student profile id** (`students.id`).
146
+ - `getParentChildUserIdsByUserId()` dan `resolveParentStudentUserScope()` mengembalikan **Core user id siswa** (`students.user_id` / `userId`). Gunakan ini untuk modul yang menyimpan relasi berdasarkan user id, misalnya LMS enrollment `studentUserId`.
147
+ - `getClasses(schoolId, params)` mendukung filter Core seperti `{ grade: 7, academic_year: '2025/2026' }`.
148
+
141
149
  Benar:
142
150
  ```ts
143
151
  this.core.get('students', schoolId)
@@ -14,6 +14,12 @@ export declare class CoreIntegrationService {
14
14
  private getHeaders;
15
15
  private makeUrl;
16
16
  private extractStudentId;
17
+ private extractStudentUserId;
18
+ private unwrapRecord;
19
+ private extractUserId;
20
+ private extractEntityId;
21
+ private matchByUserId;
22
+ private assertTenantScope;
17
23
  get<T = any>(path: string, schoolId?: string, options?: CoreRequestOptions): Promise<T>;
18
24
  post<T = any>(path: string, schoolId: string | undefined, body?: unknown, options?: CoreRequestOptions): Promise<T>;
19
25
  put<T = any>(path: string, schoolId: string | undefined, body?: unknown, options?: CoreRequestOptions): Promise<T>;
@@ -21,15 +27,38 @@ export declare class CoreIntegrationService {
21
27
  delete<T = any>(path: string, schoolId?: string, options?: CoreRequestOptions): Promise<T>;
22
28
  getParentChildrenByUserId(parentUserId: string, schoolId: string): Promise<any[]>;
23
29
  getParentChildIdsByUserId(parentUserId: string, schoolId: string): Promise<string[]>;
30
+ getParentChildUserIdsByUserId(parentUserId: string, schoolId: string): Promise<string[]>;
31
+ resolveParentStudentUserScope(parentUserId: string, schoolId: string, requestedStudentUserId?: string): Promise<string[]>;
24
32
  resolveParentStudentScope(parentUserId: string, schoolId: string, requestedStudentId?: string): Promise<string[]>;
25
33
  getStudents(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
26
34
  getStudentsByClass(classId: string, schoolId: string): Promise<any[]>;
35
+ getUser(userId: string, schoolId: string): Promise<any>;
27
36
  getStudent(studentId: string, schoolId: string): Promise<any>;
37
+ getStudentByUserId(userId: string, schoolId: string): Promise<any>;
38
+ resolveStudentUserIdToStudentId(userId: string, schoolId: string): Promise<string | null>;
28
39
  findStudentByEmail(email: string, schoolId: string): Promise<any>;
29
40
  getTeacher(teacherId: string, schoolId: string): Promise<any>;
30
- getClasses(schoolId: string): Promise<any[]>;
41
+ getTeacherByUserId(userId: string, schoolId: string): Promise<any>;
42
+ resolveTeacherUserIdToTeacherId(userId: string, schoolId: string): Promise<string | null>;
43
+ reconcileAcademicData(schoolId: string, academicYear?: string): Promise<{
44
+ teachers: any[];
45
+ classes: any[];
46
+ assignments: any[];
47
+ }>;
48
+ getTeacherAssignments(teacherId: string, schoolId: string, academicYear?: string): Promise<any[]>;
49
+ getClass(classId: string, schoolId: string): Promise<any>;
50
+ getClasses(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
31
51
  getTeachers(schoolId: string): Promise<any[]>;
32
52
  getSubjects(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
53
+ getSubject(subjectId: string, schoolId: string): Promise<any>;
54
+ getRoom(roomId: string, schoolId: string): Promise<any>;
55
+ getTimeSlots(schoolId: string): Promise<any[]>;
56
+ getActiveAcademicYear(schoolId: string): Promise<any | null>;
57
+ getActiveSemester(schoolId: string): Promise<any | null>;
58
+ getAcademicYear(yearId: string, schoolId: string): Promise<any | null>;
59
+ getSemester(semesterId: string, schoolId: string): Promise<any | null>;
60
+ getTeachingSchedule(scheduleId: string, schoolId: string): Promise<any | null>;
61
+ findTeachingSchedules(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
33
62
  getCurriculums(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
34
63
  getAcademicCalendar(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
35
64
  getRooms(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
@@ -59,6 +59,34 @@ let CoreIntegrationService = CoreIntegrationService_1 = class CoreIntegrationSer
59
59
  extractStudentId(child) {
60
60
  return child?.student_id || child?.student?.id || child?.studentUserId || child?.id || null;
61
61
  }
62
+ extractStudentUserId(child) {
63
+ return child?.student_user_id || child?.studentUserId || child?.student?.userId || child?.student?.user_id || child?.student?.user?.id || child?.userId || child?.user_id || null;
64
+ }
65
+ unwrapRecord(data) {
66
+ return data?.data || data;
67
+ }
68
+ extractUserId(entity) {
69
+ return entity?.userId || entity?.user_id || entity?.user?.id || null;
70
+ }
71
+ extractEntityId(entity) {
72
+ return entity?.id || null;
73
+ }
74
+ matchByUserId(items, userId) {
75
+ for (const item of items) {
76
+ if (item?.userId === userId)
77
+ return item;
78
+ if (item?.user_id === userId)
79
+ return item;
80
+ if (item?.user?.id === userId)
81
+ return item;
82
+ }
83
+ return null;
84
+ }
85
+ assertTenantScope(schoolId, contextLabel) {
86
+ if (!schoolId) {
87
+ throw new Error(`${contextLabel}: schoolId (X-School-ID) is required for non-super_admin operations`);
88
+ }
89
+ }
62
90
  async get(path, schoolId, options = {}) {
63
91
  const { headers, ...requestOptions } = options;
64
92
  const res = await (0, rxjs_1.firstValueFrom)(this.httpService.get(this.makeUrl(path), {
@@ -118,6 +146,25 @@ let CoreIntegrationService = CoreIntegrationService_1 = class CoreIntegrationSer
118
146
  .map((child) => this.extractStudentId(child))
119
147
  .filter((studentId) => !!studentId);
120
148
  }
149
+ async getParentChildUserIdsByUserId(parentUserId, schoolId) {
150
+ const children = await this.getParentChildrenByUserId(parentUserId, schoolId);
151
+ return children
152
+ .map((child) => this.extractStudentUserId(child))
153
+ .filter((studentUserId) => !!studentUserId);
154
+ }
155
+ async resolveParentStudentUserScope(parentUserId, schoolId, requestedStudentUserId) {
156
+ const childUserIds = await this.getParentChildUserIdsByUserId(parentUserId, schoolId);
157
+ if (childUserIds.length === 0) {
158
+ throw new common_1.ForbiddenException('Parent has no linked children with user accounts in this school');
159
+ }
160
+ if (!requestedStudentUserId) {
161
+ return childUserIds;
162
+ }
163
+ if (!childUserIds.includes(requestedStudentUserId)) {
164
+ throw new common_1.ForbiddenException('Child user is not linked to this parent');
165
+ }
166
+ return [requestedStudentUserId];
167
+ }
121
168
  async resolveParentStudentScope(parentUserId, schoolId, requestedStudentId) {
122
169
  const childIds = await this.getParentChildIdsByUserId(parentUserId, schoolId);
123
170
  if (childIds.length === 0) {
@@ -134,7 +181,8 @@ let CoreIntegrationService = CoreIntegrationService_1 = class CoreIntegrationSer
134
181
  async getStudents(schoolId, params = {}) {
135
182
  try {
136
183
  const data = await this.get('students', schoolId, { params });
137
- return data?.data || data?.students || data || [];
184
+ const body = data?.data || data || {};
185
+ return body?.data || body?.students || body || [];
138
186
  }
139
187
  catch (error) {
140
188
  if (error.response?.status === 404) {
@@ -147,15 +195,37 @@ let CoreIntegrationService = CoreIntegrationService_1 = class CoreIntegrationSer
147
195
  async getStudentsByClass(classId, schoolId) {
148
196
  return this.getStudents(schoolId, { class_id: classId, status: 'active' });
149
197
  }
198
+ async getUser(userId, schoolId) {
199
+ try {
200
+ return this.unwrapRecord(await this.get(`users/${userId}`, schoolId));
201
+ }
202
+ catch (error) {
203
+ this.logger.warn(`Failed to fetch user ${userId}: ${error.message}`);
204
+ return null;
205
+ }
206
+ }
150
207
  async getStudent(studentId, schoolId) {
151
208
  try {
152
- return await this.get(`students/${studentId}`, schoolId);
209
+ return this.unwrapRecord(await this.get(`students/${studentId}`, schoolId));
153
210
  }
154
211
  catch (error) {
155
212
  this.logger.warn(`Failed to fetch student ${studentId}: ${error.message}`);
156
213
  return null;
157
214
  }
158
215
  }
216
+ async getStudentByUserId(userId, schoolId) {
217
+ try {
218
+ return this.unwrapRecord(await this.get(`students/by-user/${userId}`, schoolId));
219
+ }
220
+ catch (error) {
221
+ this.logger.warn(`Failed to fetch student by user ${userId}: ${error.message}`);
222
+ return null;
223
+ }
224
+ }
225
+ async resolveStudentUserIdToStudentId(userId, schoolId) {
226
+ const student = await this.getStudentByUserId(userId, schoolId);
227
+ return student?.id || null;
228
+ }
159
229
  async findStudentByEmail(email, schoolId) {
160
230
  try {
161
231
  const data = await this.get('students', schoolId, { params: { q: email, limit: 1 } });
@@ -177,10 +247,71 @@ let CoreIntegrationService = CoreIntegrationService_1 = class CoreIntegrationSer
177
247
  return null;
178
248
  }
179
249
  }
180
- async getClasses(schoolId) {
250
+ async getTeacherByUserId(userId, schoolId) {
251
+ // Fast path: try direct lookup endpoint (if Core provides it)
181
252
  try {
182
- const data = await this.get('classes', schoolId);
183
- return data?.data || data || [];
253
+ const data = await this.get(`teachers/by-user/${userId}`, schoolId);
254
+ return this.unwrapRecord(data);
255
+ }
256
+ catch {
257
+ // Fallback: fetch all active teachers and find match by userId
258
+ try {
259
+ const teachers = await this.getTeachers(schoolId);
260
+ return this.matchByUserId(teachers, userId);
261
+ }
262
+ catch {
263
+ return null;
264
+ }
265
+ }
266
+ }
267
+ async resolveTeacherUserIdToTeacherId(userId, schoolId) {
268
+ const teacher = await this.getTeacherByUserId(userId, schoolId);
269
+ return this.extractEntityId(teacher);
270
+ }
271
+ async reconcileAcademicData(schoolId, academicYear) {
272
+ try {
273
+ const data = await this.post('integrations/lms/reconcile', schoolId, {
274
+ schoolId,
275
+ academicYear,
276
+ });
277
+ const body = data?.data || data;
278
+ return {
279
+ teachers: body?.teachers || [],
280
+ classes: body?.classes || [],
281
+ assignments: body?.assignments || [],
282
+ };
283
+ }
284
+ catch (error) {
285
+ this.logger.error(`Failed to reconcile academic data: ${error.message}`);
286
+ throw new Error('Core service unavailable: unable to reconcile academic data');
287
+ }
288
+ }
289
+ async getTeacherAssignments(teacherId, schoolId, academicYear) {
290
+ try {
291
+ const data = await this.get(`teachers/${teacherId}/assignments`, schoolId, {
292
+ params: academicYear ? { academicYear } : {},
293
+ });
294
+ const raw = data?.data || data;
295
+ return raw?.data || (Array.isArray(raw) ? raw : []);
296
+ }
297
+ catch {
298
+ return [];
299
+ }
300
+ }
301
+ async getClass(classId, schoolId) {
302
+ try {
303
+ return this.unwrapRecord(await this.get(`classes/${classId}`, schoolId));
304
+ }
305
+ catch (error) {
306
+ this.logger.warn(`Failed to fetch class ${classId}: ${error.message}`);
307
+ return null;
308
+ }
309
+ }
310
+ async getClasses(schoolId, params = {}) {
311
+ try {
312
+ const data = await this.get('classes', schoolId, { params });
313
+ const body = data?.data || data || {};
314
+ return body?.data || body || [];
184
315
  }
185
316
  catch (error) {
186
317
  this.logger.error(`Failed to fetch classes: ${error.message}`);
@@ -200,7 +331,91 @@ let CoreIntegrationService = CoreIntegrationService_1 = class CoreIntegrationSer
200
331
  }
201
332
  async getSubjects(schoolId, params = {}) {
202
333
  const data = await this.get('subjects', schoolId, { params });
203
- return data?.data || data || [];
334
+ const body = data?.data || data || {};
335
+ return body?.data || body || [];
336
+ }
337
+ async getSubject(subjectId, schoolId) {
338
+ try {
339
+ return this.unwrapRecord(await this.get(`subjects/${subjectId}`, schoolId));
340
+ }
341
+ catch (error) {
342
+ this.logger.warn(`Failed to fetch subject ${subjectId}: ${error.message}`);
343
+ return null;
344
+ }
345
+ }
346
+ async getRoom(roomId, schoolId) {
347
+ try {
348
+ return this.unwrapRecord(await this.get(`rooms/${roomId}`, schoolId));
349
+ }
350
+ catch (error) {
351
+ this.logger.warn(`Failed to fetch room ${roomId}: ${error.message}`);
352
+ return null;
353
+ }
354
+ }
355
+ async getTimeSlots(schoolId) {
356
+ try {
357
+ const data = await this.get('time-slots', schoolId);
358
+ return data?.data || data || [];
359
+ }
360
+ catch (error) {
361
+ this.logger.warn(`Failed to fetch time slots: ${error.message}`);
362
+ return [];
363
+ }
364
+ }
365
+ async getActiveAcademicYear(schoolId) {
366
+ try {
367
+ return this.unwrapRecord(await this.get('academic-years/active', schoolId));
368
+ }
369
+ catch (error) {
370
+ this.logger.warn(`Failed to fetch active academic year: ${error.message}`);
371
+ return null;
372
+ }
373
+ }
374
+ async getActiveSemester(schoolId) {
375
+ try {
376
+ return this.unwrapRecord(await this.get('academic-years/active-semester', schoolId));
377
+ }
378
+ catch (error) {
379
+ this.logger.warn(`Failed to fetch active semester: ${error.message}`);
380
+ return null;
381
+ }
382
+ }
383
+ async getAcademicYear(yearId, schoolId) {
384
+ try {
385
+ return this.unwrapRecord(await this.get(`academic-years/${yearId}`, schoolId));
386
+ }
387
+ catch (error) {
388
+ this.logger.warn(`Failed to fetch academic year ${yearId}: ${error.message}`);
389
+ return null;
390
+ }
391
+ }
392
+ async getSemester(semesterId, schoolId) {
393
+ try {
394
+ return this.unwrapRecord(await this.get(`academic-years/semesters/${semesterId}`, schoolId));
395
+ }
396
+ catch (error) {
397
+ this.logger.warn(`Failed to fetch semester ${semesterId}: ${error.message}`);
398
+ return null;
399
+ }
400
+ }
401
+ async getTeachingSchedule(scheduleId, schoolId) {
402
+ try {
403
+ return this.unwrapRecord(await this.get(`teaching-schedules/${scheduleId}`, schoolId));
404
+ }
405
+ catch (error) {
406
+ this.logger.warn(`Failed to fetch teaching schedule ${scheduleId}: ${error.message}`);
407
+ return null;
408
+ }
409
+ }
410
+ async findTeachingSchedules(schoolId, params = {}) {
411
+ try {
412
+ const data = await this.get('teaching-schedules', schoolId, { params });
413
+ return data?.data || data || [];
414
+ }
415
+ catch (error) {
416
+ this.logger.warn(`Failed to fetch teaching schedules: ${error.message}`);
417
+ return [];
418
+ }
204
419
  }
205
420
  async getCurriculums(schoolId, params = {}) {
206
421
  const data = await this.get('curriculum', schoolId, { params });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
- {
1
+ {
2
2
  "name": "@loka-sms/core-integration-be",
3
- "version": "0.0.1",
4
- "description": "Internal NestJS client for service-to-service access to Loka SMS Core APIs",
3
+ "version": "0.0.3",
4
+ "description": "Internal NestJS client for service-to-service access to OMNISCHOOL Core APIs",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
7
7
  "main": "dist/index.js",