sonamu 0.9.4 → 0.9.5
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/ai/providers/rtzr/utils.js +2 -2
- package/dist/database/upsert-builder.js +4 -4
- package/dist/dict/sonamu-dictionary.js +2 -2
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +2 -3
- package/dist/testing/data-explorer.d.ts.map +1 -1
- package/dist/testing/data-explorer.js +5 -3
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +3 -2
- package/dist/ui-web/assets/index-D4rYm-Xz.css +1 -0
- package/dist/ui-web/assets/{index-C5KUjXm0.js → index-DzZ7vBk4.js} +4 -4
- package/dist/ui-web/index.html +2 -2
- package/package.json +4 -4
- package/src/ai/providers/rtzr/utils.ts +1 -1
- package/src/database/upsert-builder.ts +3 -3
- package/src/dict/sonamu-dictionary.ts +1 -1
- package/src/migration/code-generation.ts +1 -6
- package/src/shared/app.shared.ts.txt +58 -3
- package/src/shared/web.shared.ts.txt +58 -3
- package/src/testing/data-explorer.ts +3 -2
- package/src/ui/api.ts +10 -1
- package/dist/ui-web/assets/index-Dr8pRJC_.css +0 -1
package/dist/ui-web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/sonamu-ui/setting.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>{{projectName}}: Sonamu UI</title>
|
|
8
|
-
<script type="module" crossorigin src="/sonamu-ui/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/sonamu-ui/assets/index-DzZ7vBk4.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-D4rYm-Xz.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"node-cron": "^4.2.1",
|
|
120
120
|
"node-sql-parser": "^5.2.0",
|
|
121
121
|
"oxfmt": "^0.43.0",
|
|
122
|
-
"oxlint": "^1.
|
|
122
|
+
"oxlint": "^1.61.0",
|
|
123
123
|
"pg": "^8.16.3",
|
|
124
124
|
"prompts": "^2.4.2",
|
|
125
125
|
"qs": "^6.14.1",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"@types/qs": "^6.14.0",
|
|
143
143
|
"nodemon": "^3.1.10",
|
|
144
144
|
"tsdown": "^0.12.5",
|
|
145
|
-
"typescript": "^6.0.
|
|
145
|
+
"typescript": "^6.0.3"
|
|
146
146
|
},
|
|
147
147
|
"peerDependencies": {
|
|
148
148
|
"@ai-sdk/anthropic": "^3.0.0",
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"ioredis": "^5.8.2",
|
|
159
159
|
"knex": "^3.2.9",
|
|
160
160
|
"pgvector": "^0.2.1",
|
|
161
|
-
"typescript": "^6.0.
|
|
161
|
+
"typescript": "^6.0.3",
|
|
162
162
|
"voyageai": "^0.0.8",
|
|
163
163
|
"zod": "^4.3.6"
|
|
164
164
|
},
|
|
@@ -310,7 +310,7 @@ export class UpsertBuilder {
|
|
|
310
310
|
|
|
311
311
|
// uuid를 별도로 보관하고, DB에 저장할 데이터에서 제거
|
|
312
312
|
const originalUuids = dataChunk.map((r) => r.uuid as string);
|
|
313
|
-
const dataForDb = dataChunk.map(({ uuid, ...rest }) => rest);
|
|
313
|
+
const dataForDb = dataChunk.map(({ uuid: _, ...rest }) => rest);
|
|
314
314
|
|
|
315
315
|
let resultRows: { id: number | string; [key: string]: unknown }[];
|
|
316
316
|
|
|
@@ -331,7 +331,7 @@ export class UpsertBuilder {
|
|
|
331
331
|
for (const row of rowsWithoutId) {
|
|
332
332
|
const values = columns.map((col) => row[col]);
|
|
333
333
|
// null이 포함된 조건은 제외 (PostgreSQL UNIQUE는 NULL 무시)
|
|
334
|
-
if (!values.some((v) => v
|
|
334
|
+
if (!values.some((v) => v === null || v === undefined)) {
|
|
335
335
|
conditions.push(values);
|
|
336
336
|
}
|
|
337
337
|
}
|
|
@@ -448,7 +448,7 @@ export class UpsertBuilder {
|
|
|
448
448
|
|
|
449
449
|
// 현재 register된 레코드들의 FK 값들 추출
|
|
450
450
|
const fkConditions = fkColumns.map((fkCol) => {
|
|
451
|
-
const fkValues = [...new Set(table.rows.map((row) => row[fkCol]).filter(
|
|
451
|
+
const fkValues = [...new Set(table.rows.map((row) => row[fkCol]).filter(Boolean))];
|
|
452
452
|
return { column: fkCol, values: fkValues };
|
|
453
453
|
});
|
|
454
454
|
|
|
@@ -631,7 +631,7 @@ export class SonamuDictionary {
|
|
|
631
631
|
const stats: Record<string, { total: number; filled: number; percent: number }> = {};
|
|
632
632
|
const total = rows.length;
|
|
633
633
|
for (const locale of locales) {
|
|
634
|
-
const filled = rows.filter((row) => row[locale]
|
|
634
|
+
const filled = rows.filter((row) => !!row[locale]).length;
|
|
635
635
|
const percent = total > 0 ? Math.round((filled / total) * 100) : 0;
|
|
636
636
|
stats[locale] = { total, filled, percent };
|
|
637
637
|
}
|
|
@@ -871,12 +871,7 @@ function genNormalColumnDefinition(column: MigrationColumn): string {
|
|
|
871
871
|
chains.push(`jsonb('${column.name}')`);
|
|
872
872
|
} else {
|
|
873
873
|
// type, length
|
|
874
|
-
|
|
875
|
-
chains.push(
|
|
876
|
-
`${column.type}('${column.name}'${
|
|
877
|
-
column.length ? `, ${column.length}` : ""
|
|
878
|
-
}${extraType ? `, '${extraType}'` : ""})`,
|
|
879
|
-
);
|
|
874
|
+
chains.push(`${column.type}('${column.name}'${column.length ? `, ${column.length}` : ""})`);
|
|
880
875
|
}
|
|
881
876
|
|
|
882
877
|
// nullable
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
import type { AxiosRequestConfig } from "axios";
|
|
13
13
|
import axios from "axios";
|
|
14
14
|
import qs from "qs";
|
|
15
|
-
import { useEffect, useRef, useState } from "react";
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
16
16
|
import { Alert } from "react-native";
|
|
17
17
|
import { type core, z } from "zod";
|
|
18
|
+
import { type InfiniteData } from "@tanstack/react-query";
|
|
18
19
|
import { getCurrentLocale } from "@/i18n/sd.generated";
|
|
19
20
|
import { ExpoEventSource as EventSource } from "@falcondev-oss/expo-event-source-polyfill";
|
|
20
21
|
|
|
@@ -510,7 +511,7 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
510
511
|
}
|
|
511
512
|
|
|
512
513
|
try {
|
|
513
|
-
const data = JSON.parse(event.data as string);
|
|
514
|
+
const data = JSON.parse(event.data as string, dateReviver);
|
|
514
515
|
handler(data);
|
|
515
516
|
} catch (error) {
|
|
516
517
|
console.error(`Failed to parse SSE data for event ${eventType}:`, error);
|
|
@@ -531,7 +532,7 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
531
532
|
}
|
|
532
533
|
|
|
533
534
|
try {
|
|
534
|
-
const data = JSON.parse(event.data as string);
|
|
535
|
+
const data = JSON.parse(event.data as string, dateReviver);
|
|
535
536
|
// 'message' 핸들러가 있으면 호출
|
|
536
537
|
const messageHandler = handlersRef.current["message" as keyof T];
|
|
537
538
|
if (messageHandler) {
|
|
@@ -584,3 +585,57 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
584
585
|
Dictionary Helper
|
|
585
586
|
*/
|
|
586
587
|
$[[dictUtils]]
|
|
588
|
+
/*
|
|
589
|
+
Query helpers
|
|
590
|
+
*/
|
|
591
|
+
type InfinitePage<TRow> = { rows: TRow[]; total: number };
|
|
592
|
+
type DedupedInfiniteData<TRow> = InfiniteData<InfinitePage<TRow>> & {
|
|
593
|
+
rows: TRow[];
|
|
594
|
+
total: number;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// useInfiniteQuery의 select에 꽂아 pages/pageParams 원본은 유지하면서
|
|
598
|
+
// 평탄화된 rows와 첫 페이지의 total을 data에 함께 노출합니다.
|
|
599
|
+
// 각 row가 id를 갖는 경우 id 기준으로 중복 제거합니다. id가 없으면 그대로 유지합니다.
|
|
600
|
+
export function dedupeAndFlatten<TRow extends { id?: unknown }>(
|
|
601
|
+
data: InfiniteData<InfinitePage<TRow>>,
|
|
602
|
+
): DedupedInfiniteData<TRow> {
|
|
603
|
+
const seen = new Set<unknown>();
|
|
604
|
+
const rows: TRow[] = [];
|
|
605
|
+
for (const page of data.pages) {
|
|
606
|
+
for (const row of page?.rows ?? []) {
|
|
607
|
+
const id = row?.id;
|
|
608
|
+
if (id !== null) {
|
|
609
|
+
if (seen.has(id)) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
seen.add(id);
|
|
613
|
+
}
|
|
614
|
+
rows.push(row);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const total = data.pages[0]?.total ?? 0;
|
|
618
|
+
return {
|
|
619
|
+
pages: data.pages,
|
|
620
|
+
pageParams: data.pageParams,
|
|
621
|
+
rows,
|
|
622
|
+
total,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// TanStack Query 결과에 수동 refresh 진입점과 새로고침 중 상태를 덧붙여 줍니다.
|
|
627
|
+
// isRefreshing은 query.isFetching과 독립적으로 이 함수 호출로 발생한 새로고침에 한정됩니다.
|
|
628
|
+
export function useRefreshable<T extends { refetch: () => Promise<unknown> }>(
|
|
629
|
+
query: T,
|
|
630
|
+
): T & { refresh: () => Promise<void>; isRefreshing: boolean } {
|
|
631
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
632
|
+
const refresh = useCallback(async () => {
|
|
633
|
+
setIsRefreshing(true);
|
|
634
|
+
try {
|
|
635
|
+
await query.refetch();
|
|
636
|
+
} finally {
|
|
637
|
+
setIsRefreshing(false);
|
|
638
|
+
}
|
|
639
|
+
}, [query]);
|
|
640
|
+
return { ...query, refresh, isRefreshing };
|
|
641
|
+
}
|
|
@@ -13,8 +13,9 @@ import type { AxiosRequestConfig } from "axios";
|
|
|
13
13
|
import axios from "axios";
|
|
14
14
|
import { EventSource } from "eventsource";
|
|
15
15
|
import qs from "qs";
|
|
16
|
-
import { useEffect, useRef, useState } from "react";
|
|
16
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
17
17
|
import { type core, z } from "zod";
|
|
18
|
+
import { type InfiniteData } from "@tanstack/react-query";
|
|
18
19
|
import { getCurrentLocale } from "@/i18n/sd.generated";
|
|
19
20
|
|
|
20
21
|
// ISO 8601 및 타임존 포맷의 날짜 문자열을 Date 객체로 변환하는 reviver
|
|
@@ -480,7 +481,7 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
480
481
|
}
|
|
481
482
|
|
|
482
483
|
try {
|
|
483
|
-
const data = JSON.parse(event.data);
|
|
484
|
+
const data = JSON.parse(event.data, dateReviver);
|
|
484
485
|
handler(data);
|
|
485
486
|
} catch (error) {
|
|
486
487
|
console.error(`Failed to parse SSE data for event ${eventType}:`, error);
|
|
@@ -501,7 +502,7 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
501
502
|
}
|
|
502
503
|
|
|
503
504
|
try {
|
|
504
|
-
const data = JSON.parse(event.data);
|
|
505
|
+
const data = JSON.parse(event.data, dateReviver);
|
|
505
506
|
// 'message' 핸들러가 있으면 호출
|
|
506
507
|
const messageHandler = handlersRef.current["message" as keyof T];
|
|
507
508
|
if (messageHandler) {
|
|
@@ -554,3 +555,57 @@ export function useSSEStream<T extends Record<string, any>>(
|
|
|
554
555
|
Dictionary Helper
|
|
555
556
|
*/
|
|
556
557
|
$[[dictUtils]]
|
|
558
|
+
/*
|
|
559
|
+
Query helpers
|
|
560
|
+
*/
|
|
561
|
+
type InfinitePage<TRow> = { rows: TRow[]; total: number };
|
|
562
|
+
type DedupedInfiniteData<TRow> = InfiniteData<InfinitePage<TRow>> & {
|
|
563
|
+
rows: TRow[];
|
|
564
|
+
total: number;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// useInfiniteQuery의 select에 꽂아 pages/pageParams 원본은 유지하면서
|
|
568
|
+
// 평탄화된 rows와 첫 페이지의 total을 data에 함께 노출합니다.
|
|
569
|
+
// 각 row가 id를 갖는 경우 id 기준으로 중복 제거합니다. id가 없으면 그대로 유지합니다.
|
|
570
|
+
export function dedupeAndFlatten<TRow extends { id?: unknown }>(
|
|
571
|
+
data: InfiniteData<InfinitePage<TRow>>,
|
|
572
|
+
): DedupedInfiniteData<TRow> {
|
|
573
|
+
const seen = new Set<unknown>();
|
|
574
|
+
const rows: TRow[] = [];
|
|
575
|
+
for (const page of data.pages) {
|
|
576
|
+
for (const row of page?.rows ?? []) {
|
|
577
|
+
const id = row?.id;
|
|
578
|
+
if (id !== null) {
|
|
579
|
+
if (seen.has(id)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
seen.add(id);
|
|
583
|
+
}
|
|
584
|
+
rows.push(row);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const total = data.pages[0]?.total ?? 0;
|
|
588
|
+
return {
|
|
589
|
+
pages: data.pages,
|
|
590
|
+
pageParams: data.pageParams,
|
|
591
|
+
rows,
|
|
592
|
+
total,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// TanStack Query 결과에 수동 refresh 진입점과 새로고침 중 상태를 덧붙여 줍니다.
|
|
597
|
+
// isRefreshing은 query.isFetching과 독립적으로 이 함수 호출로 발생한 새로고침에 한정됩니다.
|
|
598
|
+
export function useRefreshable<T extends { refetch: () => Promise<unknown> }>(
|
|
599
|
+
query: T,
|
|
600
|
+
): T & { refresh: () => Promise<void>; isRefreshing: boolean } {
|
|
601
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
602
|
+
const refresh = useCallback(async () => {
|
|
603
|
+
setIsRefreshing(true);
|
|
604
|
+
try {
|
|
605
|
+
await query.refetch();
|
|
606
|
+
} finally {
|
|
607
|
+
setIsRefreshing(false);
|
|
608
|
+
}
|
|
609
|
+
}, [query]);
|
|
610
|
+
return { ...query, refresh, isRefreshing };
|
|
611
|
+
}
|
|
@@ -4,6 +4,7 @@ import { type CacheManager } from "../cache/types";
|
|
|
4
4
|
import { type Entity } from "../entity/entity";
|
|
5
5
|
import { type EntityManager } from "../entity/entity-manager";
|
|
6
6
|
import { isBelongsToOneRelationProp, isOneToOneRelationProp, isRelationProp } from "../types/types";
|
|
7
|
+
import { nonNullable } from "../utils/utils";
|
|
7
8
|
|
|
8
9
|
export type DataExplorerStrategy = "sample" | "ids" | "query" | "file" | "recent" | "random";
|
|
9
10
|
|
|
@@ -323,7 +324,7 @@ export class DataExplorer {
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
const entity = this.entityManager.get(entityName);
|
|
326
|
-
const recordIds = records.map((r) => r.id).filter(
|
|
327
|
+
const recordIds = records.map((r) => r.id).filter(nonNullable);
|
|
327
328
|
|
|
328
329
|
// 1. Forward references: 이 entity가 참조하는 다른 entity
|
|
329
330
|
const forwardRelationProps = entity.props.filter(
|
|
@@ -346,7 +347,7 @@ export class DataExplorer {
|
|
|
346
347
|
const foreignKeyName = `${prop.name}_id`;
|
|
347
348
|
const referencedIds = records
|
|
348
349
|
.map((record) => record[foreignKeyName])
|
|
349
|
-
.filter(
|
|
350
|
+
.filter(Boolean) as number[];
|
|
350
351
|
|
|
351
352
|
if (referencedIds.length === 0) {
|
|
352
353
|
continue;
|
package/src/ui/api.ts
CHANGED
|
@@ -256,8 +256,17 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
256
256
|
const entity = EntityManager.get(entityId);
|
|
257
257
|
const subsetRows = entity.getSubsetRows();
|
|
258
258
|
|
|
259
|
+
// zod 인스턴스를 spread하면 JSON.stringify가 reference를 인라인으로 풀어내며 응답이 수백 MB까지 부풀어 V8 string limit를 초과한다.
|
|
260
|
+
const {
|
|
261
|
+
types: _types,
|
|
262
|
+
enums: _enums,
|
|
263
|
+
enumCones: _enumCones,
|
|
264
|
+
subsetCones: _subsetCones,
|
|
265
|
+
...rest
|
|
266
|
+
} = entity;
|
|
267
|
+
|
|
259
268
|
return {
|
|
260
|
-
...
|
|
269
|
+
...rest,
|
|
261
270
|
flattenSubsetRows: flattenSubsetRows(subsetRows),
|
|
262
271
|
};
|
|
263
272
|
}),
|