@coffer-org/server 2.2.1 → 2.3.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.
@@ -161,6 +161,7 @@ export const ThreadMessageSchema = new EntitySchema({
161
161
  role: { type: 'text' },
162
162
  sender: { type: 'text', nullable: true },
163
163
  text: { type: 'text' },
164
+ attachments: { type: 'text', nullable: true },
164
165
  ts: { type: 'integer' },
165
166
  reply_to_id: { type: 'text', nullable: true },
166
167
  },
@@ -145,7 +145,7 @@ export function makeTable(em, table) {
145
145
  const sources = new Set(c.from);
146
146
  for (const t of c.to) {
147
147
  if (info.columns.has(t.name) && !sources.has(t.name)) {
148
- throw new Error(`[migrations] ${table}.${t.name}: цільова колонка convert вже існує і не є джереломвикористай changeType/renameColumn`);
148
+ throw new Error(`[migrations] ${table}.${t.name}: convert target column already exists and is not a source use changeType/renameColumn`);
149
149
  }
150
150
  }
151
151
  const sqlite = dialectOf(em) === 'sqlite';
@@ -3,9 +3,16 @@ export interface StoredMsg {
3
3
  role: 'user' | 'assistant' | 'reasoning';
4
4
  sender: string | null;
5
5
  text: string;
6
+ attachments?: StoredAttachment[];
6
7
  ts: number;
7
8
  replyToId: string | null;
8
9
  }
10
+ export interface StoredAttachment {
11
+ name: string;
12
+ mime?: string;
13
+ size?: number;
14
+ label?: string;
15
+ }
9
16
  export interface ThreadChat {
10
17
  chatId: string;
11
18
  lastTs: number;
@@ -19,6 +26,7 @@ export declare function putThreadMessage(m: {
19
26
  msgId: string;
20
27
  role: 'user' | 'assistant' | 'reasoning';
21
28
  sender?: string | null;
29
+ attachments?: StoredAttachment[];
22
30
  text: string;
23
31
  ts: number;
24
32
  replyToId: string | null;
@@ -1,6 +1,27 @@
1
1
  import { getEm } from "./db.js";
2
2
  function toStored(r) {
3
- return { msgId: r.msg_id, role: r.role, sender: r.sender, text: r.text, ts: r.ts, replyToId: r.reply_to_id };
3
+ let attachments;
4
+ if (r.attachments) {
5
+ try {
6
+ const parsed = JSON.parse(r.attachments);
7
+ if (Array.isArray(parsed)) {
8
+ const valid = parsed.filter((v) => typeof v === 'object' && v !== null && typeof v.name === 'string');
9
+ if (valid.length)
10
+ attachments = valid;
11
+ }
12
+ }
13
+ catch {
14
+ }
15
+ }
16
+ return {
17
+ msgId: r.msg_id,
18
+ role: r.role,
19
+ sender: r.sender,
20
+ text: r.text,
21
+ ...(attachments ? { attachments } : {}),
22
+ ts: r.ts,
23
+ replyToId: r.reply_to_id,
24
+ };
4
25
  }
5
26
  export async function getThreadMessage(connector, chatId, msgId) {
6
27
  const em = getEm().fork();
@@ -15,6 +36,7 @@ export async function putThreadMessage(m) {
15
36
  msg_id: m.msgId,
16
37
  role: m.role,
17
38
  sender: m.sender ?? null,
39
+ attachments: m.attachments?.length ? JSON.stringify(m.attachments) : null,
18
40
  text: m.text,
19
41
  ts: m.ts,
20
42
  reply_to_id: m.replyToId,
package/dist/uploads.d.ts CHANGED
@@ -2,4 +2,14 @@ export declare function uploadsDir(): string;
2
2
  export declare function seedAsset(absPath: string): {
3
3
  name: string;
4
4
  };
5
+ export interface StoredUpload {
6
+ name: string;
7
+ mime?: string;
8
+ size: number;
9
+ }
10
+ export declare function saveUploadBytes(bytes: Uint8Array, opts?: {
11
+ originalName?: string;
12
+ mime?: string;
13
+ maxBytes?: number;
14
+ }): Promise<StoredUpload>;
5
15
  export declare function seedAssetPath(metaUrl: string): (rel: string) => string;
package/dist/uploads.js CHANGED
@@ -1,6 +1,18 @@
1
1
  import { copyFileSync, mkdirSync } from 'node:fs';
2
- import { basename, join } from 'node:path';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { basename, extname, join } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
3
5
  import { fileURLToPath } from 'node:url';
6
+ const MIME_EXT = {
7
+ 'image/jpeg': '.jpg',
8
+ 'image/png': '.png',
9
+ 'image/gif': '.gif',
10
+ 'image/webp': '.webp',
11
+ 'application/pdf': '.pdf',
12
+ 'text/plain': '.txt',
13
+ 'text/csv': '.csv',
14
+ 'application/json': '.json',
15
+ };
4
16
  export function uploadsDir() {
5
17
  const dir = process.env.UPLOADS ?? join(process.cwd(), 'data', 'uploads');
6
18
  mkdirSync(dir, { recursive: true });
@@ -11,6 +23,17 @@ export function seedAsset(absPath) {
11
23
  copyFileSync(absPath, join(uploadsDir(), name));
12
24
  return { name };
13
25
  }
26
+ export async function saveUploadBytes(bytes, opts = {}) {
27
+ const maxBytes = opts.maxBytes ?? 25 * 1024 * 1024;
28
+ if (bytes.byteLength > maxBytes)
29
+ throw new Error(`upload exceeds ${maxBytes} bytes`);
30
+ const originalExt = opts.originalName ? extname(basename(opts.originalName)).toLowerCase() : '';
31
+ const ext = originalExt && /^[.][a-z0-9]{1,10}$/.test(originalExt) ? originalExt : MIME_EXT[opts.mime ?? ''] ?? '';
32
+ const name = `${randomUUID()}${ext}`;
33
+ await writeFile(join(uploadsDir(), name), bytes);
34
+ const mime = opts.mime === 'application/octet-stream' ? undefined : opts.mime;
35
+ return { name, ...(mime ? { mime } : {}), size: bytes.byteLength };
36
+ }
14
37
  export function seedAssetPath(metaUrl) {
15
38
  return (rel) => fileURLToPath(new URL(`../../seed/assets/${rel}`, metaUrl));
16
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/server",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -24,8 +24,8 @@
24
24
  "postpack": "node ../../scripts/swap-exports.mjs src"
25
25
  },
26
26
  "dependencies": {
27
- "@coffer-org/core": "^2.1.1",
28
- "@coffer-org/sdk": "^2.1.0",
27
+ "@coffer-org/core": "^2.1.2",
28
+ "@coffer-org/sdk": "^2.1.1",
29
29
  "@extractus/oembed-extractor": "^4.1.0",
30
30
  "@fastify/cors": "^11.2.0",
31
31
  "@fastify/multipart": "^10.0.0",