@rimori/client 2.5.17 → 2.5.18-next.0

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.
@@ -1,7 +1,14 @@
1
1
  /**
2
2
  * Supported database column data types for table schema definitions.
3
3
  */
4
- type DbColumnType = 'decimal' | 'integer' | 'text' | 'boolean' | 'json' | 'timestamp' | 'uuid';
4
+ /**
5
+ * 'markdown' is stored as `text` in the database.
6
+ * Marking a column as 'markdown' causes the migration system to:
7
+ * 1. Add an `updated_at` timestamp + trigger to the table.
8
+ * 2. Add an `updated_at` trigger so the image-sync cron can detect recently
9
+ * modified entries. The cron derives which columns to scan from release.db_schema.
10
+ */
11
+ type DbColumnType = 'decimal' | 'integer' | 'text' | 'boolean' | 'json' | 'timestamp' | 'uuid' | 'markdown';
5
12
  /**
6
13
  * Foreign key relationship configuration with cascade delete support.
7
14
  * Defines a relationship where the source record is deleted when the destination record is deleted.
package/dist/index.d.ts CHANGED
@@ -18,3 +18,4 @@ export type { Theme, ApplicationMode } from './plugin/module/PluginModule';
18
18
  export type { UserInfo, Language, UserRole, ExplicitUndefined } from './plugin/module/PluginModule';
19
19
  export type { SharedContent, BasicSharedContent, ContentStatus } from './plugin/module/SharedContentController';
20
20
  export type { MacroAccomplishmentPayload, MicroAccomplishmentPayload } from './controller/AccomplishmentController';
21
+ export { StorageModule } from './plugin/module/StorageModule';
package/dist/index.js CHANGED
@@ -10,3 +10,4 @@ export * from './plugin/CommunicationHandler';
10
10
  export { setupWorker } from './worker/WorkerSetup';
11
11
  export { AudioController } from './controller/AudioController';
12
12
  export { Translator } from './controller/TranslationController';
13
+ export { StorageModule } from './plugin/module/StorageModule';
@@ -4,6 +4,7 @@ import { DbModule } from './module/DbModule';
4
4
  import { EventModule } from './module/EventModule';
5
5
  import { AIModule } from './module/AIModule';
6
6
  import { ExerciseModule } from './module/ExerciseModule';
7
+ import { StorageModule } from './module/StorageModule';
7
8
  export declare class RimoriClient {
8
9
  private static instance;
9
10
  sharedContent: SharedContentController;
@@ -12,6 +13,8 @@ export declare class RimoriClient {
12
13
  plugin: PluginModule;
13
14
  ai: AIModule;
14
15
  exercise: ExerciseModule;
16
+ /** Upload and manage images stored in Supabase via the backend. */
17
+ storage: StorageModule;
15
18
  private rimoriInfo;
16
19
  private constructor();
17
20
  static getInstance(pluginId?: string): Promise<RimoriClient>;
@@ -15,6 +15,7 @@ import { DbModule } from './module/DbModule';
15
15
  import { EventModule } from './module/EventModule';
16
16
  import { AIModule } from './module/AIModule';
17
17
  import { ExerciseModule } from './module/ExerciseModule';
18
+ import { StorageModule } from './module/StorageModule';
18
19
  import { EventBus } from '../fromRimori/EventBus';
19
20
  export class RimoriClient {
20
21
  constructor(controller, supabase, info) {
@@ -38,6 +39,7 @@ export class RimoriClient {
38
39
  this.db = new DbModule(supabase, controller, info);
39
40
  this.plugin = new PluginModule(supabase, controller, info, this.ai);
40
41
  this.exercise = new ExerciseModule(supabase, controller, info, this.event);
42
+ this.storage = new StorageModule(info.backendUrl, () => this.rimoriInfo.token);
41
43
  controller.onUpdate((updatedInfo) => {
42
44
  this.rimoriInfo = updatedInfo;
43
45
  });
@@ -67,6 +67,11 @@ export declare class AIModule {
67
67
  setSessionToken(id: string): void;
68
68
  /** Clears the stored session token (called after macro accomplishment). */
69
69
  clearSessionToken(): void;
70
+ /**
71
+ * Ensures a session token exists, requesting one from the backend if needed.
72
+ * Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
73
+ */
74
+ private ensureSessionToken;
70
75
  /** Registers a callback invoked whenever a 429 rate-limit response is received. */
71
76
  setOnRateLimited(cb: (exercisesRemaining: number) => void): void;
72
77
  /**
@@ -29,6 +29,32 @@ export class AIModule {
29
29
  clearSessionToken() {
30
30
  this.sessionTokenId = null;
31
31
  }
32
+ /**
33
+ * Ensures a session token exists, requesting one from the backend if needed.
34
+ * Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
35
+ */
36
+ ensureSessionToken() {
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ var _a, _b, _c;
39
+ if (this.sessionTokenId)
40
+ return;
41
+ const response = yield fetch(`${this.backendUrl}/ai/session`, {
42
+ method: 'POST',
43
+ headers: { Authorization: `Bearer ${this.getToken()}` },
44
+ });
45
+ if (!response.ok) {
46
+ if (response.status === 429) {
47
+ const body = yield response.json().catch(() => ({}));
48
+ const remaining = (_a = body.exercises_remaining) !== null && _a !== void 0 ? _a : 0;
49
+ (_b = this.onRateLimitedCb) === null || _b === void 0 ? void 0 : _b.call(this, remaining);
50
+ throw new Error(`Rate limit exceeded: ${(_c = body.error) !== null && _c !== void 0 ? _c : 'Daily exercise limit reached'}. exercises_remaining: ${remaining}`);
51
+ }
52
+ throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
53
+ }
54
+ const { session_token_id } = yield response.json();
55
+ this.sessionTokenId = session_token_id;
56
+ });
57
+ }
32
58
  /** Registers a callback invoked whenever a 429 rate-limit response is received. */
33
59
  setOnRateLimited(cb) {
34
60
  this.onRateLimitedCb = cb;
@@ -96,6 +122,7 @@ export class AIModule {
96
122
  getVoice(text_1) {
97
123
  return __awaiter(this, arguments, void 0, function* (text, voice = 'alloy', speed = 1, language, cache = false) {
98
124
  var _a;
125
+ yield this.ensureSessionToken();
99
126
  return yield fetch(`${this.backendUrl}/voice/tts`, {
100
127
  method: 'POST',
101
128
  headers: {
@@ -114,6 +141,7 @@ export class AIModule {
114
141
  */
115
142
  getTextFromVoice(file, language) {
116
143
  return __awaiter(this, void 0, void 0, function* () {
144
+ yield this.ensureSessionToken();
117
145
  const formData = new FormData();
118
146
  formData.append('file', file);
119
147
  if (language) {
@@ -45,8 +45,9 @@ export declare class ExerciseModule {
45
45
  */
46
46
  view(): Promise<Exercise[]>;
47
47
  /**
48
- * Creates a new exercise or multiple exercises via the backend API.
49
- * When creating multiple exercises, all requests are made in parallel but only one event is emitted.
48
+ * Creates one or more exercises via the backend API.
49
+ * Multiple exercises are sent in a single bulk request to ensure atomicity —
50
+ * either all succeed or none are inserted.
50
51
  * @param params Exercise creation parameters (single or array).
51
52
  * @returns Created exercise objects.
52
53
  */
@@ -37,31 +37,30 @@ export class ExerciseModule {
37
37
  });
38
38
  }
39
39
  /**
40
- * Creates a new exercise or multiple exercises via the backend API.
41
- * When creating multiple exercises, all requests are made in parallel but only one event is emitted.
40
+ * Creates one or more exercises via the backend API.
41
+ * Multiple exercises are sent in a single bulk request to ensure atomicity —
42
+ * either all succeed or none are inserted.
42
43
  * @param params Exercise creation parameters (single or array).
43
44
  * @returns Created exercise objects.
44
45
  */
45
46
  add(params) {
46
47
  return __awaiter(this, void 0, void 0, function* () {
47
48
  const exercises = Array.isArray(params) ? params : [params];
48
- const responses = yield Promise.all(exercises.map((exercise) => __awaiter(this, void 0, void 0, function* () {
49
- const response = yield fetch(`${this.backendUrl}/exercises`, {
50
- method: 'POST',
51
- headers: {
52
- 'Content-Type': 'application/json',
53
- Authorization: `Bearer ${this.token}`,
54
- },
55
- body: JSON.stringify(exercise),
56
- });
57
- if (!response.ok) {
58
- const errorText = yield response.text();
59
- throw new Error(`Failed to create exercise: ${errorText}`);
60
- }
61
- return yield response.json();
62
- })));
49
+ const response = yield fetch(`${this.backendUrl}/exercises`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ Authorization: `Bearer ${this.token}`,
54
+ },
55
+ body: JSON.stringify({ exercises }),
56
+ });
57
+ if (!response.ok) {
58
+ const errorText = yield response.text();
59
+ throw new Error(`Failed to create exercises: ${errorText}`);
60
+ }
61
+ const data = yield response.json();
63
62
  this.eventModule.emit('global.exercises.triggerChange');
64
- return responses;
63
+ return data;
65
64
  });
66
65
  }
67
66
  /**
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Storage module for plugin image operations.
3
+ *
4
+ * Handles uploading images to Supabase storage via the backend.
5
+ *
6
+ * Images are tracked automatically: the backend cron scans markdown columns
7
+ * (declared via `type: 'markdown'` in db.config.ts) every 30 minutes,
8
+ * confirms images found in content, and deletes orphaned ones. No plugin-side
9
+ * confirm or delete calls are needed.
10
+ */
11
+ export declare class StorageModule {
12
+ private readonly backendUrl;
13
+ private readonly getToken;
14
+ constructor(backendUrl: string, getToken: () => string);
15
+ /**
16
+ * Upload a PNG image blob to Supabase storage via the backend.
17
+ *
18
+ * The image is initially "unconfirmed". The background cron will link it to
19
+ * the entry automatically when it scans the markdown column after the entry
20
+ * is saved (within ~30 minutes).
21
+ *
22
+ * @returns `{ data: { url, path } }` on success, `{ error }` on failure.
23
+ */
24
+ uploadImage(pngBlob: Blob): Promise<{
25
+ data: {
26
+ url: string;
27
+ path: string;
28
+ };
29
+ error?: undefined;
30
+ } | {
31
+ data?: undefined;
32
+ error: Error;
33
+ }>;
34
+ }
@@ -0,0 +1,57 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ /**
11
+ * Storage module for plugin image operations.
12
+ *
13
+ * Handles uploading images to Supabase storage via the backend.
14
+ *
15
+ * Images are tracked automatically: the backend cron scans markdown columns
16
+ * (declared via `type: 'markdown'` in db.config.ts) every 30 minutes,
17
+ * confirms images found in content, and deletes orphaned ones. No plugin-side
18
+ * confirm or delete calls are needed.
19
+ */
20
+ export class StorageModule {
21
+ constructor(backendUrl, getToken) {
22
+ this.backendUrl = backendUrl;
23
+ this.getToken = getToken;
24
+ }
25
+ /**
26
+ * Upload a PNG image blob to Supabase storage via the backend.
27
+ *
28
+ * The image is initially "unconfirmed". The background cron will link it to
29
+ * the entry automatically when it scans the markdown column after the entry
30
+ * is saved (within ~30 minutes).
31
+ *
32
+ * @returns `{ data: { url, path } }` on success, `{ error }` on failure.
33
+ */
34
+ uploadImage(pngBlob) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ var _a;
37
+ const formData = new FormData();
38
+ formData.append('file', pngBlob, 'image.png');
39
+ try {
40
+ const response = yield fetch(`${this.backendUrl}/plugin-images/upload`, {
41
+ method: 'POST',
42
+ headers: { Authorization: `Bearer ${this.getToken()}` },
43
+ body: formData,
44
+ });
45
+ if (!response.ok) {
46
+ const body = (yield response.json().catch(() => ({})));
47
+ return { error: new Error((_a = body.message) !== null && _a !== void 0 ? _a : `Upload failed (${response.status})`) };
48
+ }
49
+ const result = (yield response.json());
50
+ return { data: result };
51
+ }
52
+ catch (err) {
53
+ return { error: err instanceof Error ? err : new Error(String(err)) };
54
+ }
55
+ });
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.17",
3
+ "version": "2.5.18-next.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {