create-entity-server 0.0.9 → 0.0.23
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/bin/create.js +26 -8
- package/package.json +1 -1
- package/template/.env.example +20 -3
- package/template/configs/database.json +173 -10
- package/template/configs/jwt.json +1 -0
- package/template/configs/oauth.json +37 -0
- package/template/configs/push.json +26 -0
- package/template/entities/Account/account_audit.json +4 -5
- package/template/entities/README.md +4 -4
- package/template/entities/{Auth → System/Auth}/account.json +0 -14
- package/template/entities/System/system_audit_log.json +14 -8
- package/template/samples/README.md +43 -21
- package/template/samples/browser/entity-server-client.js +453 -0
- package/template/samples/browser/example.html +498 -0
- package/template/samples/entities/01_basic_fields.json +39 -0
- package/template/samples/entities/02_types_and_defaults.json +67 -0
- package/template/samples/entities/03_hash_and_unique.json +33 -0
- package/template/samples/entities/04_fk_and_composite_unique.json +29 -0
- package/template/samples/entities/05_cache.json +55 -0
- package/template/samples/entities/06_history_and_hard_delete.json +60 -0
- package/template/samples/entities/07_license_scope.json +52 -0
- package/template/samples/entities/08_hook_sql.json +52 -0
- package/template/samples/entities/09_hook_entity.json +65 -0
- package/template/samples/entities/10_hook_submit_delete.json +78 -0
- package/template/samples/entities/11_hook_webhook.json +84 -0
- package/template/samples/entities/12_hook_push.json +73 -0
- package/template/samples/entities/13_read_only.json +54 -0
- package/template/samples/entities/14_optimistic_lock.json +29 -0
- package/template/samples/entities/15_reset_defaults.json +94 -0
- package/template/samples/entities/16_isolated_license.json +62 -0
- package/template/samples/entities/README.md +91 -0
- package/template/samples/flutter/lib/entity_server_client.dart +261 -48
- package/template/samples/java/EntityServerClient.java +325 -61
- package/template/samples/java/EntityServerExample.java +4 -3
- package/template/samples/kotlin/EntityServerClient.kt +261 -45
- package/template/samples/node/src/EntityServerClient.js +348 -59
- package/template/samples/node/src/example.js +9 -9
- package/template/samples/php/ci4/Config/EntityServer.php +14 -0
- package/template/samples/php/ci4/Controllers/EntityController.php +202 -0
- package/template/samples/php/ci4/Controllers/ProductController.php +16 -76
- package/template/samples/php/ci4/Libraries/EntityServer.php +352 -60
- package/template/samples/php/laravel/Services/EntityServerService.php +245 -40
- package/template/samples/python/entity_server.py +287 -68
- package/template/samples/python/example.py +7 -6
- package/template/samples/react/src/example.tsx +41 -25
- package/template/samples/swift/EntityServerClient.swift +248 -37
- package/template/scripts/normalize-entities.sh +10 -10
- package/template/scripts/run.ps1 +12 -3
- package/template/scripts/run.sh +120 -37
- package/template/scripts/update-server.ps1 +160 -4
- package/template/scripts/update-server.sh +132 -4
- package/template/samples/react/src/api/entityServerClient.ts +0 -290
- package/template/samples/react/src/hooks/useEntity.ts +0 -105
- /package/template/entities/{Auth → System/Auth}/api_keys.json +0 -0
- /package/template/entities/{Auth → System/Auth}/license.json +0 -0
- /package/template/entities/{Auth → System/Auth}/rbac_roles.json +0 -0
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity Server API 클라이언트 (React / 브라우저)
|
|
3
|
-
*
|
|
4
|
-
* 브라우저 환경에서는 HMAC secret을 노출할 수 없으므로 JWT Bearer 토큰 방식을 사용합니다.
|
|
5
|
-
*
|
|
6
|
-
* 패킷 암호화 지원:
|
|
7
|
-
* 서버가 application/octet-stream 으로 응답하면 자동으로 복호화합니다.
|
|
8
|
-
* 복호화 키: sha256(access_token)
|
|
9
|
-
* 의존성: @noble/ciphers, @noble/hashes (npm install @noble/ciphers @noble/hashes)
|
|
10
|
-
*
|
|
11
|
-
* 환경변수 (Vite):
|
|
12
|
-
* VITE_ENTITY_SERVER_URL=http://localhost:47200
|
|
13
|
-
* VITE_PACKET_MAGIC_LEN=4 (서버 packet_magic_len 과 동일하게)
|
|
14
|
-
*
|
|
15
|
-
* 트랜잭션 사용 예:
|
|
16
|
-
* await es.transStart();
|
|
17
|
-
* try {
|
|
18
|
-
* const orderRef = await es.submit('order', { user_seq: 1, total: 9900 }); // seq: "$tx.0"
|
|
19
|
-
* await es.submit('order_item', { order_seq: orderRef.seq, item_seq: 5 }); // "$tx.0" 자동 치환
|
|
20
|
-
* const result = await es.transCommit();
|
|
21
|
-
* const orderSeq = result.results[0].seq; // 실제 seq
|
|
22
|
-
* } catch (e) {
|
|
23
|
-
* await es.transRollback();
|
|
24
|
-
* }
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { xchacha20_poly1305 } from "@noble/ciphers/chacha";
|
|
28
|
-
import { sha256 } from "@noble/hashes/sha2";
|
|
29
|
-
|
|
30
|
-
export interface EntityListParams {
|
|
31
|
-
page?: number;
|
|
32
|
-
limit?: number;
|
|
33
|
-
orderBy?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface EntityQueryFilter {
|
|
37
|
-
field: string;
|
|
38
|
-
op: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "like" | "in";
|
|
39
|
-
value: unknown;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class EntityServerClient {
|
|
43
|
-
private baseUrl: string;
|
|
44
|
-
private token: string;
|
|
45
|
-
private magicLen: number;
|
|
46
|
-
private activeTxId: string | null = null;
|
|
47
|
-
|
|
48
|
-
constructor(baseUrl?: string, token?: string) {
|
|
49
|
-
this.baseUrl = (
|
|
50
|
-
baseUrl ??
|
|
51
|
-
(import.meta as unknown as Record<string, Record<string, string>>)
|
|
52
|
-
.env?.VITE_ENTITY_SERVER_URL ??
|
|
53
|
-
"http://localhost:47200"
|
|
54
|
-
).replace(/\/$/, "");
|
|
55
|
-
this.token = token ?? "";
|
|
56
|
-
const envMagic = (
|
|
57
|
-
import.meta as unknown as Record<string, Record<string, string>>
|
|
58
|
-
).env?.VITE_PACKET_MAGIC_LEN;
|
|
59
|
-
this.magicLen = envMagic ? Number(envMagic) : 4;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
setToken(token: string): void {
|
|
63
|
-
this.token = token;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ─── 인증 ────────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
async login(
|
|
69
|
-
email: string,
|
|
70
|
-
password: string,
|
|
71
|
-
): Promise<{
|
|
72
|
-
access_token: string;
|
|
73
|
-
refresh_token: string;
|
|
74
|
-
expires_in: number;
|
|
75
|
-
}> {
|
|
76
|
-
const data = await this.request<{
|
|
77
|
-
data: {
|
|
78
|
-
access_token: string;
|
|
79
|
-
refresh_token: string;
|
|
80
|
-
expires_in: number;
|
|
81
|
-
};
|
|
82
|
-
}>("POST", "/v1/auth/login", { email, passwd: password }, false);
|
|
83
|
-
this.token = data.data.access_token;
|
|
84
|
-
return data.data;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async refreshToken(
|
|
88
|
-
refreshToken: string,
|
|
89
|
-
): Promise<{ access_token: string; expires_in: number }> {
|
|
90
|
-
const data = await this.request<{
|
|
91
|
-
data: { access_token: string; expires_in: number };
|
|
92
|
-
}>("POST", "/v1/auth/refresh", { refresh_token: refreshToken }, false);
|
|
93
|
-
this.token = data.data.access_token;
|
|
94
|
-
return data.data;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ─── 트랜잭션 ──────────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 트랜잭션 시작 — 서버에 트랜잭션 큐를 등록하고 transaction_id 를 반환합니다.
|
|
101
|
-
* 이후 submit / delete 가 서버 큐에 쌓이고 transCommit() 시 일괄 처리됩니다.
|
|
102
|
-
*/
|
|
103
|
-
async transStart(): Promise<string> {
|
|
104
|
-
const res = await this.request<{ ok: boolean; transaction_id: string }>(
|
|
105
|
-
"POST",
|
|
106
|
-
"/v1/transaction/start",
|
|
107
|
-
undefined,
|
|
108
|
-
false,
|
|
109
|
-
);
|
|
110
|
-
this.activeTxId = res.transaction_id;
|
|
111
|
-
return this.activeTxId;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 트랜잭션 단위로 변경사항을 롤백합니다.
|
|
116
|
-
* transactionId 생략 시 transStart() 로 시작한 활성 트랜잭션을 사용합니다.
|
|
117
|
-
*/
|
|
118
|
-
transRollback(transactionId?: string): Promise<{ ok: boolean }> {
|
|
119
|
-
const txId = transactionId ?? this.activeTxId;
|
|
120
|
-
if (!txId)
|
|
121
|
-
return Promise.reject(
|
|
122
|
-
new Error("No active transaction. Call transStart() first."),
|
|
123
|
-
);
|
|
124
|
-
this.activeTxId = null;
|
|
125
|
-
return this.request("POST", `/v1/transaction/rollback/${txId}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 트랜잭션 커밋 — 서버 큐에 쌓인 작업을 단일 DB 트랜잭션으로 일괄 처리합니다.
|
|
130
|
-
* transactionId 생략 시 transStart() 로 시작한 활성 트랜잭션을 사용합니다.
|
|
131
|
-
*/
|
|
132
|
-
transCommit(
|
|
133
|
-
transactionId?: string,
|
|
134
|
-
): Promise<{ ok: boolean; results: unknown[] }> {
|
|
135
|
-
const txId = transactionId ?? this.activeTxId;
|
|
136
|
-
if (!txId)
|
|
137
|
-
return Promise.reject(
|
|
138
|
-
new Error("No active transaction. Call transStart() first."),
|
|
139
|
-
);
|
|
140
|
-
this.activeTxId = null;
|
|
141
|
-
return this.request("POST", `/v1/transaction/commit/${txId}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
get<T = unknown>(
|
|
147
|
-
entity: string,
|
|
148
|
-
seq: number,
|
|
149
|
-
): Promise<{ ok: boolean; data: T }> {
|
|
150
|
-
return this.request("GET", `/v1/entity/${entity}/${seq}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
list<T = unknown>(
|
|
154
|
-
entity: string,
|
|
155
|
-
params: EntityListParams = {},
|
|
156
|
-
): Promise<{ ok: boolean; data: T[]; total: number }> {
|
|
157
|
-
const q = buildQuery({ page: 1, limit: 20, ...params });
|
|
158
|
-
return this.request("GET", `/v1/entity/${entity}/list?${q}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
count(entity: string): Promise<{ ok: boolean; count: number }> {
|
|
162
|
-
return this.request("GET", `/v1/entity/${entity}/count`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
query<T = unknown>(
|
|
166
|
-
entity: string,
|
|
167
|
-
filter: EntityQueryFilter[] = [],
|
|
168
|
-
params: EntityListParams = {},
|
|
169
|
-
): Promise<{ ok: boolean; data: T[]; total: number }> {
|
|
170
|
-
const q = buildQuery({ page: 1, limit: 20, ...params });
|
|
171
|
-
return this.request("POST", `/v1/entity/${entity}/query?${q}`, filter);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
submit<T = unknown>(
|
|
175
|
-
entity: string,
|
|
176
|
-
data: Record<string, unknown>,
|
|
177
|
-
opts: { transactionId?: string } = {},
|
|
178
|
-
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
179
|
-
const txId = opts.transactionId ?? this.activeTxId;
|
|
180
|
-
const extra = txId ? { "X-Transaction-ID": txId } : {};
|
|
181
|
-
return this.request(
|
|
182
|
-
"POST",
|
|
183
|
-
`/v1/entity/${entity}/submit`,
|
|
184
|
-
data,
|
|
185
|
-
true,
|
|
186
|
-
extra,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
delete(
|
|
191
|
-
entity: string,
|
|
192
|
-
seq: number,
|
|
193
|
-
opts: { transactionId?: string; hard?: boolean } = {},
|
|
194
|
-
): Promise<{ ok: boolean }> {
|
|
195
|
-
const q = opts.hard ? "?hard=true" : "";
|
|
196
|
-
const txId = opts.transactionId ?? this.activeTxId;
|
|
197
|
-
const extra = txId ? { "X-Transaction-ID": txId } : {};
|
|
198
|
-
return this.request(
|
|
199
|
-
"DELETE",
|
|
200
|
-
`/v1/entity/${entity}/delete/${seq}${q}`,
|
|
201
|
-
undefined,
|
|
202
|
-
true,
|
|
203
|
-
extra,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
history<T = unknown>(
|
|
208
|
-
entity: string,
|
|
209
|
-
seq: number,
|
|
210
|
-
params: EntityListParams = {},
|
|
211
|
-
): Promise<{ ok: boolean; data: T[] }> {
|
|
212
|
-
const q = buildQuery({ page: 1, limit: 50, ...params });
|
|
213
|
-
return this.request("GET", `/v1/entity/${entity}/history/${seq}?${q}`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {
|
|
217
|
-
return this.request(
|
|
218
|
-
"POST",
|
|
219
|
-
`/v1/entity/${entity}/rollback/${historySeq}`,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private async request<T>(
|
|
224
|
-
method: string,
|
|
225
|
-
path: string,
|
|
226
|
-
body?: unknown,
|
|
227
|
-
withAuth = true,
|
|
228
|
-
extraHeaders: Record<string, string> = {},
|
|
229
|
-
): Promise<T> {
|
|
230
|
-
const headers: Record<string, string> = {
|
|
231
|
-
"Content-Type": "application/json",
|
|
232
|
-
...extraHeaders,
|
|
233
|
-
};
|
|
234
|
-
if (withAuth && this.token) {
|
|
235
|
-
headers["Authorization"] = `Bearer ${this.token}`;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const res = await fetch(this.baseUrl + path, {
|
|
239
|
-
method,
|
|
240
|
-
headers,
|
|
241
|
-
...(body != null ? { body: JSON.stringify(body) } : {}),
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const contentType = res.headers.get("Content-Type") ?? "";
|
|
245
|
-
|
|
246
|
-
// 패킷 암호화 응답: application/octet-stream → 복호화
|
|
247
|
-
if (contentType.includes("application/octet-stream")) {
|
|
248
|
-
const buffer = await res.arrayBuffer();
|
|
249
|
-
return this.decryptPacket<T>(buffer);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const data = await res.json();
|
|
253
|
-
if (!data.ok) {
|
|
254
|
-
const err = new Error(
|
|
255
|
-
data.message ?? `EntityServer error (HTTP ${res.status})`,
|
|
256
|
-
);
|
|
257
|
-
(err as { status?: number }).status = res.status;
|
|
258
|
-
throw err;
|
|
259
|
-
}
|
|
260
|
-
return data as T;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* XChaCha20-Poly1305 패킷 복호화
|
|
265
|
-
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
266
|
-
* 키: sha256(access_token)
|
|
267
|
-
*/
|
|
268
|
-
private decryptPacket<T>(buffer: ArrayBuffer): T {
|
|
269
|
-
const key = sha256(new TextEncoder().encode(this.token));
|
|
270
|
-
const data = new Uint8Array(buffer);
|
|
271
|
-
const nonce = data.slice(this.magicLen, this.magicLen + 24);
|
|
272
|
-
const ciphertext = data.slice(this.magicLen + 24);
|
|
273
|
-
const cipher = xchacha20_poly1305(key, nonce);
|
|
274
|
-
const plaintext = cipher.decrypt(ciphertext);
|
|
275
|
-
return JSON.parse(new TextDecoder().decode(plaintext)) as T;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function buildQuery(params: Record<string, unknown>): string {
|
|
280
|
-
return Object.entries(params)
|
|
281
|
-
.filter(([, v]) => v != null)
|
|
282
|
-
.map(
|
|
283
|
-
([k, v]) =>
|
|
284
|
-
`${encodeURIComponent(k === "orderBy" ? "order_by" : k)}=${encodeURIComponent(String(v))}`,
|
|
285
|
-
)
|
|
286
|
-
.join("&");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** 싱글턴 인스턴스 (앱 전체 공유) */
|
|
290
|
-
export const entityServer = new EntityServerClient();
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity Server React 훅
|
|
3
|
-
*
|
|
4
|
-
* @tanstack/react-query 기반
|
|
5
|
-
* 설치: npm install @tanstack/react-query
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
9
|
-
import type {
|
|
10
|
-
EntityListParams,
|
|
11
|
-
EntityQueryFilter,
|
|
12
|
-
} from "../api/entityServerClient";
|
|
13
|
-
import { entityServer } from "../api/entityServerClient";
|
|
14
|
-
|
|
15
|
-
// ─── 조회 훅 ─────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
/** 단건 조회 */
|
|
18
|
-
export function useEntityGet<T = unknown>(entity: string, seq: number | null) {
|
|
19
|
-
return useQuery({
|
|
20
|
-
queryKey: ["entity", entity, seq],
|
|
21
|
-
queryFn: () => entityServer.get<T>(entity, seq!),
|
|
22
|
-
enabled: seq != null,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** 목록 조회 */
|
|
27
|
-
export function useEntityList<T = unknown>(
|
|
28
|
-
entity: string,
|
|
29
|
-
params: EntityListParams = {},
|
|
30
|
-
) {
|
|
31
|
-
return useQuery({
|
|
32
|
-
queryKey: ["entity", entity, "list", params],
|
|
33
|
-
queryFn: () => entityServer.list<T>(entity, params),
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 건수 조회 */
|
|
38
|
-
export function useEntityCount(entity: string) {
|
|
39
|
-
return useQuery({
|
|
40
|
-
queryKey: ["entity", entity, "count"],
|
|
41
|
-
queryFn: () => entityServer.count(entity),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 필터 검색 */
|
|
46
|
-
export function useEntityQuery<T = unknown>(
|
|
47
|
-
entity: string,
|
|
48
|
-
filter: EntityQueryFilter[],
|
|
49
|
-
params: EntityListParams = {},
|
|
50
|
-
) {
|
|
51
|
-
return useQuery({
|
|
52
|
-
queryKey: ["entity", entity, "query", filter, params],
|
|
53
|
-
queryFn: () => entityServer.query<T>(entity, filter, params),
|
|
54
|
-
enabled: filter.length > 0,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 변경 이력 조회 */
|
|
59
|
-
export function useEntityHistory<T = unknown>(
|
|
60
|
-
entity: string,
|
|
61
|
-
seq: number | null,
|
|
62
|
-
) {
|
|
63
|
-
return useQuery({
|
|
64
|
-
queryKey: ["entity", entity, seq, "history"],
|
|
65
|
-
queryFn: () => entityServer.history<T>(entity, seq!),
|
|
66
|
-
enabled: seq != null,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─── 뮤테이션 훅 ─────────────────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
/** 생성 또는 수정 */
|
|
73
|
-
export function useEntitySubmit(entity: string) {
|
|
74
|
-
const qc = useQueryClient();
|
|
75
|
-
return useMutation({
|
|
76
|
-
mutationFn: (data: Record<string, unknown>) =>
|
|
77
|
-
entityServer.submit(entity, data),
|
|
78
|
-
onSuccess: () => {
|
|
79
|
-
qc.invalidateQueries({ queryKey: ["entity", entity] });
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 삭제 */
|
|
85
|
-
export function useEntityDelete(entity: string) {
|
|
86
|
-
const qc = useQueryClient();
|
|
87
|
-
return useMutation({
|
|
88
|
-
mutationFn: (seq: number) => entityServer.delete(entity, seq),
|
|
89
|
-
onSuccess: () => {
|
|
90
|
-
qc.invalidateQueries({ queryKey: ["entity", entity] });
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** 롤백 */
|
|
96
|
-
export function useEntityRollback(entity: string) {
|
|
97
|
-
const qc = useQueryClient();
|
|
98
|
-
return useMutation({
|
|
99
|
-
mutationFn: (historySeq: number) =>
|
|
100
|
-
entityServer.rollback(entity, historySeq),
|
|
101
|
-
onSuccess: () => {
|
|
102
|
-
qc.invalidateQueries({ queryKey: ["entity", entity] });
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|