sonamu 0.10.3 → 0.10.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/bin/cli.js +12 -1
- package/dist/database/puri.d.ts +4 -1
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +18 -1
- package/dist/database/puri.types.d.ts +12 -1
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +1 -1
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +2 -3
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite-reporter.js +10 -2
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +8 -3
- package/dist/ui-web/assets/{index-CDd6xT-F.js → index-BLX4zN8c.js} +1 -1
- package/dist/ui-web/index.html +1 -1
- package/package.json +3 -3
- package/src/bin/cli.ts +15 -2
- package/src/database/__tests__/puri.test.ts +66 -0
- package/src/database/puri.ts +40 -0
- package/src/database/puri.types.test-d.ts +115 -1
- package/src/database/puri.types.ts +19 -0
- package/src/migration/postgresql-schema-reader.ts +1 -2
- package/src/naite/naite-reporter.ts +10 -1
- package/src/skills/sonamu/puri.md +28 -0
- package/src/template/__tests__/generated.template.search-text.test.ts +17 -2
- package/src/template/implementations/generated.template.ts +15 -1
package/dist/ui-web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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-
|
|
8
|
+
<script type="module" crossorigin src="/sonamu-ui/assets/index-BLX4zN8c.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-Dx4ap5i4.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -131,8 +131,8 @@
|
|
|
131
131
|
"vitest": "^4.1.9",
|
|
132
132
|
"@sonamu-kit/hmr-hook": "^0.5.1",
|
|
133
133
|
"@sonamu-kit/hmr-runner": "^0.2.0",
|
|
134
|
-
"@sonamu-kit/
|
|
135
|
-
"@sonamu-kit/
|
|
134
|
+
"@sonamu-kit/tasks": "^0.3.1",
|
|
135
|
+
"@sonamu-kit/ts-loader": "^2.2.0"
|
|
136
136
|
},
|
|
137
137
|
"devDependencies": {
|
|
138
138
|
"@types/bcrypt": "^6",
|
package/src/bin/cli.ts
CHANGED
|
@@ -197,8 +197,8 @@ async function bootstrap() {
|
|
|
197
197
|
stub_entity,
|
|
198
198
|
scaffold_model,
|
|
199
199
|
scaffold_model_test,
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
scaffold_view_list,
|
|
201
|
+
scaffold_view_form,
|
|
202
202
|
cone_gen,
|
|
203
203
|
sync,
|
|
204
204
|
build_all,
|
|
@@ -983,6 +983,19 @@ async function scaffold_model_test(entityId: string) {
|
|
|
983
983
|
});
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
+
async function scaffold_view_list(entityId: string) {
|
|
987
|
+
await Sonamu.syncer.generateTemplate("view_list", {
|
|
988
|
+
entityId,
|
|
989
|
+
extra: undefined,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function scaffold_view_form(entityId: string) {
|
|
994
|
+
await Sonamu.syncer.generateTemplate("view_form", {
|
|
995
|
+
entityId,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
986
999
|
/**
|
|
987
1000
|
* pnpm sonamu skills sync 하면 실행되는 함수입니다.
|
|
988
1001
|
* 공식 Skills를 로컬 프로젝트 또는 글로벌 ~/.claude/로 동기화합니다.
|
|
@@ -7,6 +7,16 @@ type TestSchema = {
|
|
|
7
7
|
users: {
|
|
8
8
|
id: number;
|
|
9
9
|
name: string;
|
|
10
|
+
payload: {
|
|
11
|
+
actor: {
|
|
12
|
+
id: string;
|
|
13
|
+
role: string;
|
|
14
|
+
};
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
tags: string[];
|
|
18
|
+
state: string;
|
|
19
|
+
readonly __json__: readonly ["payload", "tags", "state"];
|
|
10
20
|
};
|
|
11
21
|
};
|
|
12
22
|
|
|
@@ -33,3 +43,59 @@ describe("Puri locking methods", () => {
|
|
|
33
43
|
expect(query).toBe('select * from "users" where "id" = 1 limit 1 for share');
|
|
34
44
|
});
|
|
35
45
|
});
|
|
46
|
+
|
|
47
|
+
describe("Puri JSONB containment", () => {
|
|
48
|
+
it("object array scalar and hostile-looking values remain serialized bindings", () => {
|
|
49
|
+
const hostileValue = `x' OR 1=1 --`;
|
|
50
|
+
const objectQuery = usersQuery()
|
|
51
|
+
.whereJsonSupersetOf("users.payload", {
|
|
52
|
+
actor: { id: hostileValue },
|
|
53
|
+
})
|
|
54
|
+
.rawQuery()
|
|
55
|
+
.toSQL();
|
|
56
|
+
const arrayQuery = usersQuery().whereJsonSupersetOf("tags", [hostileValue]).rawQuery().toSQL();
|
|
57
|
+
const scalarQuery = usersQuery().whereJsonSupersetOf("state", hostileValue).rawQuery().toSQL();
|
|
58
|
+
|
|
59
|
+
expect(objectQuery.sql).toBe('select * from "users" where "users"."payload" @> ?');
|
|
60
|
+
expect(objectQuery.bindings).toEqual([
|
|
61
|
+
JSON.stringify({
|
|
62
|
+
actor: { id: hostileValue },
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
expect(arrayQuery.sql).toBe('select * from "users" where "tags" @> ?');
|
|
66
|
+
expect(arrayQuery.bindings).toEqual([JSON.stringify([hostileValue])]);
|
|
67
|
+
expect(scalarQuery.sql).toBe('select * from "users" where "state" @> ?');
|
|
68
|
+
expect(scalarQuery.bindings).toEqual([JSON.stringify(hostileValue)]);
|
|
69
|
+
|
|
70
|
+
expect(objectQuery.sql).not.toContain(hostileValue);
|
|
71
|
+
expect(arrayQuery.sql).not.toContain(hostileValue);
|
|
72
|
+
expect(scalarQuery.sql).not.toContain(hostileValue);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("WhereGroup preserves AND OR ordering and undefined is rejected", () => {
|
|
76
|
+
const groupedQuery = usersQuery()
|
|
77
|
+
.where("id", 1)
|
|
78
|
+
.whereGroup((group) => {
|
|
79
|
+
group
|
|
80
|
+
.whereJsonSupersetOf("payload", { actor: { role: "admin" } })
|
|
81
|
+
.orWhereJsonSupersetOf("tags", ["urgent"]);
|
|
82
|
+
})
|
|
83
|
+
.rawQuery()
|
|
84
|
+
.toSQL();
|
|
85
|
+
|
|
86
|
+
expect(groupedQuery.sql).toBe(
|
|
87
|
+
'select * from "users" where "id" = ? and ("payload" @> ? or "tags" @> ?)',
|
|
88
|
+
);
|
|
89
|
+
expect(groupedQuery.bindings).toEqual([
|
|
90
|
+
1,
|
|
91
|
+
JSON.stringify({ actor: { role: "admin" } }),
|
|
92
|
+
JSON.stringify(["urgent"]),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const query = usersQuery();
|
|
96
|
+
expect(() => Reflect.apply(query.whereJsonSupersetOf, query, ["payload", undefined])).toThrow(
|
|
97
|
+
"Puri JSONB containment value must be JSON-serializable; JSON.stringify returned undefined.",
|
|
98
|
+
);
|
|
99
|
+
expect(query.rawQuery().toSQL().sql).toBe('select * from "users"');
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/database/puri.ts
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
type FuzzyOperator,
|
|
21
21
|
type InsertData,
|
|
22
22
|
type InsertResult,
|
|
23
|
+
type JsonColumns,
|
|
24
|
+
type JsonSupersetValue,
|
|
23
25
|
type LeftJoinedMarker,
|
|
24
26
|
type LeftJoinMarkerFor,
|
|
25
27
|
type NumericColumns,
|
|
@@ -70,6 +72,18 @@ function normalizeFuzzyOperator(operator?: string): FuzzyOperator {
|
|
|
70
72
|
return fuzzyOperator;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
function serializeJsonSupersetValue(value: unknown): string {
|
|
76
|
+
const serialized = JSON.stringify(value);
|
|
77
|
+
|
|
78
|
+
if (serialized === undefined) {
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
"Puri JSONB containment value must be JSON-serializable; JSON.stringify returned undefined.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return serialized;
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
function normalizeOrderByDirection(direction: PuriOrderByDirection = "asc"): PuriOrderByDirection {
|
|
74
88
|
if (direction !== "asc" && direction !== "desc") {
|
|
75
89
|
throw new Error(`Invalid order direction: ${direction}`);
|
|
@@ -784,6 +798,15 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
784
798
|
return this as any;
|
|
785
799
|
}
|
|
786
800
|
|
|
801
|
+
// WHERE JSONB SUPERSET (@>)
|
|
802
|
+
whereJsonSupersetOf<TColumn extends JsonColumns<TTables>>(
|
|
803
|
+
column: TColumn,
|
|
804
|
+
value: JsonSupersetValue<ExtractColumnType<TTables, TColumn>>,
|
|
805
|
+
): this {
|
|
806
|
+
this.knexQuery.whereJsonSupersetOf(column, serializeJsonSupersetValue(value));
|
|
807
|
+
return this;
|
|
808
|
+
}
|
|
809
|
+
|
|
787
810
|
// WHERE MATCH
|
|
788
811
|
whereMatch<TColumn extends FulltextColumns<TTables>>(column: TColumn, value: string): this {
|
|
789
812
|
this.knexQuery.whereRaw(`MATCH (${String(column)}) AGAINST (?)`, [value]);
|
|
@@ -1449,6 +1472,23 @@ export class WhereGroup<TTables extends Record<string, any>> {
|
|
|
1449
1472
|
return this;
|
|
1450
1473
|
}
|
|
1451
1474
|
|
|
1475
|
+
// WHERE JSONB SUPERSET (@>)
|
|
1476
|
+
whereJsonSupersetOf<TColumn extends JsonColumns<TTables>>(
|
|
1477
|
+
column: TColumn,
|
|
1478
|
+
value: JsonSupersetValue<ExtractColumnType<TTables, TColumn>>,
|
|
1479
|
+
): this {
|
|
1480
|
+
this.builder.whereJsonSupersetOf(column, serializeJsonSupersetValue(value));
|
|
1481
|
+
return this;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
orWhereJsonSupersetOf<TColumn extends JsonColumns<TTables>>(
|
|
1485
|
+
column: TColumn,
|
|
1486
|
+
value: JsonSupersetValue<ExtractColumnType<TTables, TColumn>>,
|
|
1487
|
+
): this {
|
|
1488
|
+
this.builder.orWhereJsonSupersetOf(column, serializeJsonSupersetValue(value));
|
|
1489
|
+
return this;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1452
1492
|
// WHERE MATCH
|
|
1453
1493
|
whereMatch<TColumn extends FulltextColumns<TTables>>(column: TColumn, value: string): this;
|
|
1454
1494
|
whereMatch(...args: any[]): this {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { describe, expectTypeOf, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Puri } from "./puri";
|
|
4
4
|
import {
|
|
5
5
|
type AvailableColumns,
|
|
6
6
|
type ExtractColumnType,
|
|
7
|
+
type JsonColumns,
|
|
8
|
+
type JsonSupersetValue,
|
|
7
9
|
type LeftJoinedMarker,
|
|
8
10
|
type ParseSelectObject,
|
|
9
11
|
} from "./puri.types";
|
|
@@ -18,6 +20,28 @@ type MockSchema = {
|
|
|
18
20
|
name: string;
|
|
19
21
|
email: string;
|
|
20
22
|
department_id: number | null;
|
|
23
|
+
preferences: {
|
|
24
|
+
theme: "light" | "dark";
|
|
25
|
+
notifications: {
|
|
26
|
+
email: boolean;
|
|
27
|
+
sms: boolean;
|
|
28
|
+
};
|
|
29
|
+
labels: string[];
|
|
30
|
+
sessions: {
|
|
31
|
+
id: string;
|
|
32
|
+
active: boolean;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
json_tags: string[];
|
|
36
|
+
json_scalar: string;
|
|
37
|
+
nullable_payload: {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
} | null;
|
|
40
|
+
native_tags: string[];
|
|
41
|
+
created_at: Date;
|
|
42
|
+
embedding: number[];
|
|
43
|
+
readonly __json__: readonly ["preferences", "json_tags", "json_scalar", "nullable_payload"];
|
|
44
|
+
readonly __vector__: readonly ["embedding"];
|
|
21
45
|
};
|
|
22
46
|
departments: {
|
|
23
47
|
id: number;
|
|
@@ -458,6 +482,96 @@ describe("AvailableColumns", () => {
|
|
|
458
482
|
});
|
|
459
483
|
});
|
|
460
484
|
|
|
485
|
+
describe("JsonColumns and JsonSupersetValue", () => {
|
|
486
|
+
it("JSON metadata selects only JSON columns and preserves recursive RHS types", () => {
|
|
487
|
+
type Tables = { users: MockSchema["users"] };
|
|
488
|
+
type Columns = JsonColumns<Tables>;
|
|
489
|
+
type PreferencesValue = JsonSupersetValue<MockSchema["users"]["preferences"]>;
|
|
490
|
+
type NullablePayloadValue = JsonSupersetValue<MockSchema["users"]["nullable_payload"]>;
|
|
491
|
+
|
|
492
|
+
expectTypeOf<Columns>().toEqualTypeOf<
|
|
493
|
+
| "users.preferences"
|
|
494
|
+
| "users.json_tags"
|
|
495
|
+
| "users.json_scalar"
|
|
496
|
+
| "users.nullable_payload"
|
|
497
|
+
| "preferences"
|
|
498
|
+
| "json_tags"
|
|
499
|
+
| "json_scalar"
|
|
500
|
+
| "nullable_payload"
|
|
501
|
+
>();
|
|
502
|
+
|
|
503
|
+
const partialObject: PreferencesValue = {
|
|
504
|
+
notifications: { email: true },
|
|
505
|
+
sessions: [{ active: false }],
|
|
506
|
+
};
|
|
507
|
+
const arrayRoot: JsonSupersetValue<MockSchema["users"]["json_tags"]> = ["urgent"];
|
|
508
|
+
const scalarRoot: JsonSupersetValue<MockSchema["users"]["json_scalar"]> = "active";
|
|
509
|
+
const nullableColumnValue: NullablePayloadValue = { enabled: true };
|
|
510
|
+
|
|
511
|
+
expectTypeOf(partialObject).toExtend<PreferencesValue>();
|
|
512
|
+
expectTypeOf(arrayRoot).toEqualTypeOf<string[]>();
|
|
513
|
+
expectTypeOf(scalarRoot).toEqualTypeOf<string>();
|
|
514
|
+
expectTypeOf(nullableColumnValue).toExtend<NullablePayloadValue>();
|
|
515
|
+
|
|
516
|
+
// @ts-expect-error nested boolean property must not accept a string.
|
|
517
|
+
const wrongNestedType: PreferencesValue = { notifications: { email: "yes" } };
|
|
518
|
+
// @ts-expect-error array element types are preserved.
|
|
519
|
+
const wrongArrayElement: JsonSupersetValue<MockSchema["users"]["json_tags"]> = [1];
|
|
520
|
+
// @ts-expect-error object array elements retain their recursive partial shape.
|
|
521
|
+
const wrongObjectArrayElement: PreferencesValue = { sessions: [{ active: "yes" }] };
|
|
522
|
+
// @ts-expect-error nullable JSON columns do not accept top-level null as a containment value.
|
|
523
|
+
const nullRoot: NullablePayloadValue = null;
|
|
524
|
+
// @ts-expect-error undefined cannot be serialized as a JSONB containment value.
|
|
525
|
+
const undefinedRoot: PreferencesValue = undefined;
|
|
526
|
+
|
|
527
|
+
const unknownValue: unknown = { notifications: { email: true } };
|
|
528
|
+
// @ts-expect-error unknown must be narrowed before it can be used as a containment value.
|
|
529
|
+
const unknownRoot: PreferencesValue = unknownValue;
|
|
530
|
+
|
|
531
|
+
expectTypeOf(wrongNestedType).toExtend<PreferencesValue>();
|
|
532
|
+
expectTypeOf(wrongArrayElement).toEqualTypeOf<string[]>();
|
|
533
|
+
expectTypeOf(wrongObjectArrayElement).toExtend<PreferencesValue>();
|
|
534
|
+
expectTypeOf(nullRoot).toExtend<NullablePayloadValue>();
|
|
535
|
+
expectTypeOf(undefinedRoot).toExtend<PreferencesValue>();
|
|
536
|
+
expectTypeOf(unknownRoot).toExtend<PreferencesValue>();
|
|
537
|
+
|
|
538
|
+
type Query = Puri<MockSchema, Tables, MockSchema["users"]>;
|
|
539
|
+
const query = {} as Query;
|
|
540
|
+
|
|
541
|
+
expectTypeOf(
|
|
542
|
+
query.whereJsonSupersetOf("preferences", {
|
|
543
|
+
notifications: { sms: false },
|
|
544
|
+
}),
|
|
545
|
+
).toEqualTypeOf<Query>();
|
|
546
|
+
expectTypeOf(
|
|
547
|
+
query.whereGroup((group) =>
|
|
548
|
+
group
|
|
549
|
+
.whereJsonSupersetOf("users.preferences", { theme: "dark" })
|
|
550
|
+
.orWhereJsonSupersetOf("users.json_tags", ["urgent"]),
|
|
551
|
+
),
|
|
552
|
+
).toEqualTypeOf<Query>();
|
|
553
|
+
|
|
554
|
+
// @ts-expect-error ordinary string columns are not JSON columns.
|
|
555
|
+
query.whereJsonSupersetOf("name", "Noa");
|
|
556
|
+
// @ts-expect-error date columns are not JSON columns.
|
|
557
|
+
query.whereJsonSupersetOf("created_at", new Date());
|
|
558
|
+
// @ts-expect-error native PostgreSQL array columns are not JSON columns.
|
|
559
|
+
query.whereJsonSupersetOf("native_tags", ["urgent"]);
|
|
560
|
+
// @ts-expect-error vector columns are not JSON columns.
|
|
561
|
+
query.whereJsonSupersetOf("embedding", [0.1, 0.2]);
|
|
562
|
+
// @ts-expect-error scalar JSON roots retain their scalar type.
|
|
563
|
+
query.whereJsonSupersetOf("json_scalar", { state: "active" });
|
|
564
|
+
// @ts-expect-error top-level null is rejected even when the JSON column itself is nullable.
|
|
565
|
+
query.whereJsonSupersetOf("nullable_payload", null);
|
|
566
|
+
// @ts-expect-error top-level undefined is not JSON-serializable.
|
|
567
|
+
query.whereJsonSupersetOf("preferences", undefined);
|
|
568
|
+
// @ts-expect-error unknown must be narrowed before calling the public method.
|
|
569
|
+
query.whereJsonSupersetOf("preferences", unknownValue);
|
|
570
|
+
// @ts-expect-error top-level OR is intentionally exposed only through whereGroup.
|
|
571
|
+
query.orWhereJsonSupersetOf("preferences", { theme: "light" });
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
461
575
|
describe("Puri locking methods", () => {
|
|
462
576
|
it("forUpdate 체이닝 후 first 결과 타입을 유지한다", () => {
|
|
463
577
|
type Query = Puri<MockSchema, { users: MockSchema["users"] }, MockSchema["users"]>;
|
|
@@ -16,6 +16,7 @@ type LeftJoinedKey = "__leftJoined__";
|
|
|
16
16
|
type HasDefault = "__hasDefault__";
|
|
17
17
|
type GeneratedKey = "__generated__";
|
|
18
18
|
type VectorKey = "__vector__";
|
|
19
|
+
type JsonKey = "__json__";
|
|
19
20
|
type VirtualQueryKey = "__virtual_query__";
|
|
20
21
|
|
|
21
22
|
type InternalTypeKeys =
|
|
@@ -25,6 +26,7 @@ type InternalTypeKeys =
|
|
|
25
26
|
| HasDefault
|
|
26
27
|
| GeneratedKey
|
|
27
28
|
| VectorKey
|
|
29
|
+
| JsonKey
|
|
28
30
|
| VirtualQueryKey;
|
|
29
31
|
|
|
30
32
|
// ============================================
|
|
@@ -42,6 +44,23 @@ export type VectorColumns<TTables extends Record<string, any>> =
|
|
|
42
44
|
}[keyof TTables]
|
|
43
45
|
| (IsSingleKey<TTables> extends true ? VectorColumnKeys<TTables[keyof TTables]> : never);
|
|
44
46
|
|
|
47
|
+
// __json__ 메타데이터에서 JSON 컬럼 추출
|
|
48
|
+
type JsonColumnKeys<T> = T extends { [K in JsonKey]: readonly (infer J)[] } ? J & string : never;
|
|
49
|
+
|
|
50
|
+
export type JsonColumns<TTables extends Record<string, any>> =
|
|
51
|
+
| {
|
|
52
|
+
[TAlias in keyof TTables]: `${TAlias & string}.${JsonColumnKeys<TTables[TAlias]>}`;
|
|
53
|
+
}[keyof TTables]
|
|
54
|
+
| (IsSingleKey<TTables> extends true ? JsonColumnKeys<TTables[keyof TTables]> : never);
|
|
55
|
+
|
|
56
|
+
type JsonSupersetNestedValue<T> = T extends readonly (infer Element)[]
|
|
57
|
+
? JsonSupersetNestedValue<Element>[]
|
|
58
|
+
: T extends object
|
|
59
|
+
? { [K in keyof T]?: JsonSupersetNestedValue<T[K]> }
|
|
60
|
+
: T;
|
|
61
|
+
|
|
62
|
+
export type JsonSupersetValue<T> = JsonSupersetNestedValue<NonNullable<T>>;
|
|
63
|
+
|
|
45
64
|
// 테이블명 타입
|
|
46
65
|
export type TableName<TSchema> = keyof TSchema & string;
|
|
47
66
|
|
|
@@ -42,6 +42,12 @@ function getSocketPath(): string {
|
|
|
42
42
|
: join(homedir(), ".sonamu", `naite-${hash}.sock`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 연결 실패가 지속될 때(extension 미실행 등) buffer가 무한히 커지지 않도록
|
|
47
|
+
* 최근 메시지만 이 개수만큼 유지합니다.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_BUFFERED_MESSAGES = 1000;
|
|
50
|
+
|
|
45
51
|
class NaiteReporterClass {
|
|
46
52
|
private socketPath: string | null = null;
|
|
47
53
|
private socket: Socket | null = null;
|
|
@@ -97,8 +103,11 @@ class NaiteReporterClass {
|
|
|
97
103
|
if (this.connected && this.socket) {
|
|
98
104
|
this.socket.write(msg);
|
|
99
105
|
} else {
|
|
100
|
-
// 연결 대기 중이면 버퍼에
|
|
106
|
+
// 연결 대기 중이면 버퍼에 저장, 상한 초과 시 오래된 메시지부터 폐기
|
|
101
107
|
this.buffer.push(msg);
|
|
108
|
+
if (this.buffer.length > MAX_BUFFERED_MESSAGES) {
|
|
109
|
+
this.buffer.shift();
|
|
110
|
+
}
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
@@ -171,6 +171,34 @@ db.where("status", "active").orWhereGroup((g) => {
|
|
|
171
171
|
});
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
### JSONB Containment (`@>`)
|
|
175
|
+
|
|
176
|
+
Use `whereJsonSupersetOf()` to require a JSONB column to contain a JSON value. The column and
|
|
177
|
+
containment value are checked against the generated Entity type, and the value is serialized and
|
|
178
|
+
bound internally.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
db.table("products").whereJsonSupersetOf("metadata", { warranty: 2 });
|
|
182
|
+
|
|
183
|
+
// Aliased columns are supported.
|
|
184
|
+
db.table({ p: "products" }).whereJsonSupersetOf("p.metadata", {
|
|
185
|
+
tags: ["featured"],
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Inside a WHERE group, both AND and OR variants are available:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
db.whereGroup((g) => {
|
|
193
|
+
g.where("status", "active").orWhereJsonSupersetOf("metadata", {
|
|
194
|
+
tags: ["featured"],
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Do not call `JSON.stringify()` yourself. For other JSONB operators such as `->>`, `->`, and `?`,
|
|
200
|
+
continue to use parameterized `whereRaw()`.
|
|
201
|
+
|
|
174
202
|
## JOIN
|
|
175
203
|
|
|
176
204
|
```typescript
|
|
@@ -62,6 +62,8 @@ async function registerEntityWithFileCustomScalar() {
|
|
|
62
62
|
table: `generated_template_file_${entitySeq}`,
|
|
63
63
|
props: [
|
|
64
64
|
{ name: "id", type: "integer" as const },
|
|
65
|
+
{ name: "title", type: "string" as const },
|
|
66
|
+
{ name: "created_at", type: "date" as const },
|
|
65
67
|
{
|
|
66
68
|
name: "file_props",
|
|
67
69
|
type: "json" as const,
|
|
@@ -70,7 +72,7 @@ async function registerEntityWithFileCustomScalar() {
|
|
|
70
72
|
],
|
|
71
73
|
indexes: [],
|
|
72
74
|
subsets: {
|
|
73
|
-
A: ["id", "file_props"],
|
|
75
|
+
A: ["id", "title", "created_at", "file_props"],
|
|
74
76
|
},
|
|
75
77
|
enums: {},
|
|
76
78
|
};
|
|
@@ -82,7 +84,7 @@ async function registerEntityWithFileCustomScalar() {
|
|
|
82
84
|
image: SonamuFileSchema.nullable(),
|
|
83
85
|
});
|
|
84
86
|
|
|
85
|
-
return { typeId };
|
|
87
|
+
return { entity: registeredEntity, typeId };
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
describe("Template__generated searchText", () => {
|
|
@@ -151,3 +153,16 @@ describe("Template__generated searchText", () => {
|
|
|
151
153
|
);
|
|
152
154
|
});
|
|
153
155
|
});
|
|
156
|
+
|
|
157
|
+
describe("Template__generated JSON metadata", () => {
|
|
158
|
+
test("only type=json properties appear in readonly __json__", async () => {
|
|
159
|
+
const { entity } = await registerEntityWithFileCustomScalar();
|
|
160
|
+
const template = new Template__generated();
|
|
161
|
+
|
|
162
|
+
const source = template.getBaseSchemaSourceCode(entity).lines.join("\n");
|
|
163
|
+
|
|
164
|
+
expect(source).toContain('readonly __json__: readonly ["file_props"],');
|
|
165
|
+
expect(source).not.toContain('readonly __json__: readonly ["title"');
|
|
166
|
+
expect(source).not.toContain('readonly __json__: readonly ["created_at"');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -215,6 +215,14 @@ export class Template__generated extends Template {
|
|
|
215
215
|
.filter((prop) => prop.type === "vector" || prop.type === "vector[]")
|
|
216
216
|
.map((prop) => prop.name);
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* JSON props
|
|
220
|
+
* - json 타입인 컬럼
|
|
221
|
+
*/
|
|
222
|
+
const jsonColumns = entity.props
|
|
223
|
+
.filter((prop) => prop.type === "json")
|
|
224
|
+
.map((prop) => prop.name);
|
|
225
|
+
|
|
218
226
|
/**
|
|
219
227
|
* generated props
|
|
220
228
|
* - generated 속성이 있는 컬럼 (INSERT/UPDATE 시 값 제공 불가)
|
|
@@ -232,7 +240,8 @@ export class Template__generated extends Template {
|
|
|
232
240
|
virtualQueryProps.length > 0 ||
|
|
233
241
|
hasDefaultColumns.length > 0 ||
|
|
234
242
|
generatedColumns.length > 0 ||
|
|
235
|
-
hasVectorColumns.length > 0
|
|
243
|
+
hasVectorColumns.length > 0 ||
|
|
244
|
+
jsonColumns.length > 0;
|
|
236
245
|
|
|
237
246
|
const lines = [
|
|
238
247
|
`export const ${schemaName} = ${schemaBody};`,
|
|
@@ -264,6 +273,11 @@ export class Template__generated extends Template {
|
|
|
264
273
|
? `readonly __vector__: readonly [${hasVectorColumns
|
|
265
274
|
.map((col) => `"${col}"`)
|
|
266
275
|
.join(", ")}],`
|
|
276
|
+
: "") +
|
|
277
|
+
(jsonColumns.length > 0
|
|
278
|
+
? `readonly __json__: readonly [${jsonColumns
|
|
279
|
+
.map((col) => `"${col}"`)
|
|
280
|
+
.join(", ")}],`
|
|
267
281
|
: "")
|
|
268
282
|
}}`
|
|
269
283
|
: "") +
|