akanjs 2.0.5 → 2.0.7
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/cli/application/application.command.ts +4 -1
- package/cli/application/application.runner.ts +6 -8
- package/cli/build.ts +3 -1
- package/cli/cloud/cloud.runner.ts +7 -8
- package/cli/index.js +288 -115
- package/cli/library/library.runner.ts +2 -2
- package/cli/module/module.runner.ts +2 -2
- package/cli/npmRegistry.ts +13 -0
- package/cli/openBrowser.ts +15 -0
- package/cli/pluralizeName.ts +5 -0
- package/cli/scalar/scalar.prompt.ts +2 -2
- package/cli/scalar/scalar.runner.ts +2 -2
- package/cli/semver.ts +18 -0
- package/cli/templates/lib/sig.ts +2 -2
- package/cli/workspace/workspace.runner.ts +3 -3
- package/client/cookie.ts +10 -15
- package/common/index.ts +1 -0
- package/common/jwtDecode.ts +17 -0
- package/constant/serialize.ts +1 -1
- package/devkit/akanApp/akanApp.host.ts +46 -9
- package/devkit/akanConfig/akanConfig.ts +2 -1
- package/devkit/capacitor.base.config.ts +18 -4
- package/devkit/capacitorApp.ts +118 -64
- package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
- package/devkit/mobile/mobileTarget.ts +2 -1
- package/devkit/scanInfo.ts +1 -0
- package/document/dataLoader.ts +140 -6
- package/document/database.ts +1 -1
- package/package.json +7 -13
- package/server/akanApp.ts +250 -44
- package/server/di/diLifecycle.ts +1 -1
- package/server/processMetricsCollector.ts +79 -1
- package/server/proxy/localeWebProxy.ts +29 -12
- package/server/resolver/database.resolver.ts +82 -31
- package/server/resolver/signal.resolver.ts +67 -28
- package/service/ipcTypes.ts +5 -0
- package/service/predefinedAdaptor/database.adaptor.ts +95 -27
- package/service/predefinedAdaptor/solidSqlite.ts +7 -7
- package/service/predefinedAdaptor/storage.adaptor.ts +35 -9
- package/service/serviceModule.ts +1 -6
- package/signal/base.signal.ts +1 -1
- package/signal/index.ts +1 -0
- package/signal/middleware.ts +5 -1
- package/signal/signalContext.ts +85 -31
- package/signal/signalRegistry.ts +35 -10
- package/signal/trace.ts +279 -0
- package/types/cli/npmRegistry.d.ts +1 -0
- package/types/cli/openBrowser.d.ts +1 -0
- package/types/cli/pluralizeName.d.ts +1 -0
- package/types/cli/semver.d.ts +1 -0
- package/types/client/cookie.d.ts +6 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/jwtDecode.d.ts +2 -0
- package/types/devkit/capacitorApp.d.ts +14 -5
- package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
- package/types/document/dataLoader.d.ts +21 -2
- package/types/document/database.d.ts +1 -1
- package/types/server/processMetricsCollector.d.ts +2 -0
- package/types/service/ipcTypes.d.ts +5 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +26 -32
- package/types/service/predefinedAdaptor/solidSqlite.d.ts +3 -3
- package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -2
- package/types/service/serviceModule.d.ts +1 -1
- package/types/signal/index.d.ts +1 -0
- package/types/signal/signalContext.d.ts +4 -1
- package/types/signal/signalRegistry.d.ts +25 -4
- package/types/signal/trace.d.ts +97 -0
- package/types/ui/Signal/style.d.ts +15 -0
- package/ui/Signal/Arg.tsx +22 -15
- package/ui/Signal/Doc.tsx +30 -24
- package/ui/Signal/Listener.tsx +15 -39
- package/ui/Signal/Message.tsx +32 -50
- package/ui/Signal/Object.tsx +16 -13
- package/ui/Signal/PubSub.tsx +29 -47
- package/ui/Signal/Response.tsx +7 -17
- package/ui/Signal/RestApi.tsx +41 -57
- package/ui/Signal/WebSocket.tsx +1 -1
- package/ui/Signal/style.ts +36 -0
- package/webkit/useCsrValues.ts +147 -37
|
@@ -74,23 +74,17 @@ export interface DocumentStore {
|
|
|
74
74
|
bulkWrite(
|
|
75
75
|
operations: { updateOne: { filter: DocumentQuery; update: DocumentUpdate; upsert?: boolean } }[],
|
|
76
76
|
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
|
|
77
|
-
find(
|
|
78
|
-
query?: DocumentQuery,
|
|
79
|
-
options?: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number },
|
|
80
|
-
): Promise<any[]>;
|
|
77
|
+
find(query?: DocumentQuery, options?: FindManyOptions): Promise<any[]>;
|
|
81
78
|
findIds(
|
|
82
79
|
query?: DocumentQuery,
|
|
83
80
|
options?: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number },
|
|
84
81
|
): Promise<string[]>;
|
|
85
|
-
findOne(
|
|
86
|
-
query?: DocumentQuery,
|
|
87
|
-
options?: { sort?: SortOption; skip?: number | null; sample?: boolean },
|
|
88
|
-
): Promise<any | null>;
|
|
82
|
+
findOne(query?: DocumentQuery, options?: FindOneOptions): Promise<any | null>;
|
|
89
83
|
findId(
|
|
90
84
|
query?: DocumentQuery,
|
|
91
85
|
options?: { sort?: SortOption; skip?: number | null; sample?: boolean },
|
|
92
86
|
): Promise<string | null>;
|
|
93
|
-
pickOne(query?: DocumentQuery, options?:
|
|
87
|
+
pickOne(query?: DocumentQuery, options?: FindOneOptions): Promise<any>;
|
|
94
88
|
pickById(id: string): Promise<any>;
|
|
95
89
|
exists(query?: DocumentQuery): Promise<string | null>;
|
|
96
90
|
count(query?: DocumentQuery): Promise<number>;
|
|
@@ -137,6 +131,15 @@ type DocumentRecord = Record<string, unknown>;
|
|
|
137
131
|
type MutableDocumentRecord = Record<string, unknown>;
|
|
138
132
|
type FieldMap = Record<string, { getProps: () => Record<string, unknown>; [key: string]: unknown }>;
|
|
139
133
|
type SortOption = Record<string, 1 | -1> | null | undefined;
|
|
134
|
+
type ProjectionOption = Partial<Record<string, boolean>> | null | undefined;
|
|
135
|
+
type FindManyOptions = {
|
|
136
|
+
sort?: SortOption;
|
|
137
|
+
skip?: number | null;
|
|
138
|
+
limit?: number | null;
|
|
139
|
+
sample?: number;
|
|
140
|
+
select?: ProjectionOption;
|
|
141
|
+
};
|
|
142
|
+
type FindOneOptions = { sort?: SortOption; skip?: number | null; sample?: boolean; select?: ProjectionOption };
|
|
140
143
|
type QueryOperatorName = Exclude<
|
|
141
144
|
DocumentQueryNode,
|
|
142
145
|
{ kind: "all" } | { kind: "any" } | { kind: "not" } | { kind: "raw" }
|
|
@@ -148,6 +151,7 @@ interface SqliteDocumentRow {
|
|
|
148
151
|
removedAt?: number | string | null;
|
|
149
152
|
_doc: string;
|
|
150
153
|
}
|
|
154
|
+
type ProjectedSqliteDocumentRow = Omit<SqliteDocumentRow, "_doc"> & Record<string, unknown>;
|
|
151
155
|
|
|
152
156
|
interface DocumentDatabaseOwner {
|
|
153
157
|
getConnection(): AkanSqlClient;
|
|
@@ -510,6 +514,7 @@ export class SqliteDocumentStore {
|
|
|
510
514
|
readonly table: string;
|
|
511
515
|
readonly compiler: QueryCompiler;
|
|
512
516
|
#insertStmt: AkanSqlStatement | null = null;
|
|
517
|
+
#readStmtCache = new Map<string, AkanSqlStatement>();
|
|
513
518
|
|
|
514
519
|
constructor(
|
|
515
520
|
private readonly owner: DocumentDatabaseOwner,
|
|
@@ -637,20 +642,23 @@ export class SqliteDocumentStore {
|
|
|
637
642
|
return { acknowledged: true, matchedCount, modifiedCount, upsertedId };
|
|
638
643
|
}
|
|
639
644
|
|
|
640
|
-
async find(
|
|
641
|
-
query?: DocumentQuery,
|
|
642
|
-
options: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number } = {},
|
|
643
|
-
) {
|
|
645
|
+
async find(query?: DocumentQuery, options: FindManyOptions = {}) {
|
|
644
646
|
const { where, params } = this.safeQuery(query);
|
|
645
647
|
const limitValue = Number(options.limit ?? 0);
|
|
646
648
|
const skipValue = Number(options.skip ?? 0);
|
|
647
649
|
const limit = limitValue ? ` LIMIT ${limitValue}` : "";
|
|
648
650
|
const offset = skipValue ? ` OFFSET ${skipValue}` : "";
|
|
649
651
|
const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
652
|
+
const projection = this.normalizeProjection(options.select);
|
|
653
|
+
if (projection) {
|
|
654
|
+
const rows = await this.prepareReadStmt(
|
|
655
|
+
`SELECT ${this.projectionSql(projection)} FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
|
|
656
|
+
).all<ProjectedSqliteDocumentRow>(...params);
|
|
657
|
+
return rows.map((row) => this.fromProjectedRow(row, projection));
|
|
658
|
+
}
|
|
659
|
+
const rows = await this.prepareReadStmt(
|
|
660
|
+
`SELECT * FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
|
|
661
|
+
).all<SqliteDocumentRow>(...params);
|
|
654
662
|
return rows.map((row) => this.hydrate(this.fromRow(row)));
|
|
655
663
|
}
|
|
656
664
|
|
|
@@ -664,14 +672,13 @@ export class SqliteDocumentStore {
|
|
|
664
672
|
const limit = limitValue ? ` LIMIT ${limitValue}` : "";
|
|
665
673
|
const offset = skipValue ? ` OFFSET ${skipValue}` : "";
|
|
666
674
|
const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
|
|
667
|
-
const rows = await this.
|
|
668
|
-
.
|
|
669
|
-
|
|
670
|
-
.all<{ id: string }>(...params);
|
|
675
|
+
const rows = await this.prepareReadStmt(
|
|
676
|
+
`SELECT "id" FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
|
|
677
|
+
).all<{ id: string }>(...params);
|
|
671
678
|
return rows.map((row) => row.id);
|
|
672
679
|
}
|
|
673
680
|
|
|
674
|
-
async findOne(query?: DocumentQuery, options:
|
|
681
|
+
async findOne(query?: DocumentQuery, options: FindOneOptions = {}) {
|
|
675
682
|
return (await this.find(query, { ...options, limit: 1, sample: options.sample ? 1 : undefined })).at(0) ?? null;
|
|
676
683
|
}
|
|
677
684
|
|
|
@@ -679,7 +686,7 @@ export class SqliteDocumentStore {
|
|
|
679
686
|
return (await this.findIds(query, { ...options, limit: 1, sample: options.sample ? 1 : undefined })).at(0) ?? null;
|
|
680
687
|
}
|
|
681
688
|
|
|
682
|
-
async pickOne(query?: DocumentQuery, options:
|
|
689
|
+
async pickOne(query?: DocumentQuery, options: FindOneOptions = {}) {
|
|
683
690
|
const doc = await this.findOne(query, options);
|
|
684
691
|
if (!doc) throw new Error(`No Document (${this.table}): ${JSON.stringify(query)}`);
|
|
685
692
|
return doc;
|
|
@@ -697,10 +704,9 @@ export class SqliteDocumentStore {
|
|
|
697
704
|
|
|
698
705
|
async count(query?: DocumentQuery) {
|
|
699
706
|
const { where, params } = this.safeQuery(query);
|
|
700
|
-
const row = await this.
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
.get<{ count: number }>(...params);
|
|
707
|
+
const row = await this.prepareReadStmt(
|
|
708
|
+
`SELECT count(*) as count FROM ${quoteIdent(this.table)} WHERE ${where}`,
|
|
709
|
+
).get<{ count: number }>(...params);
|
|
704
710
|
return row?.count ?? 0;
|
|
705
711
|
}
|
|
706
712
|
|
|
@@ -901,6 +907,55 @@ export class SqliteDocumentStore {
|
|
|
901
907
|
};
|
|
902
908
|
}
|
|
903
909
|
|
|
910
|
+
private normalizeProjection(select: ProjectionOption): string[] | null {
|
|
911
|
+
if (!select) return null;
|
|
912
|
+
const fields = Object.entries(select)
|
|
913
|
+
.filter(([, included]) => included)
|
|
914
|
+
.map(([field]) => field);
|
|
915
|
+
if (!fields.length) return null;
|
|
916
|
+
return [...new Set(fields.filter((field) => field !== "_doc"))];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private projectionSql(fields: string[]) {
|
|
920
|
+
const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
|
|
921
|
+
const baseColumns = [...BASE_COLUMNS].map((field) => quoteIdent(field));
|
|
922
|
+
const jsonColumns = jsonFields.map(
|
|
923
|
+
(field, idx) => `${this.compiler.fieldExpr(field)} AS ${quoteIdent(this.projectionAlias(idx))}`,
|
|
924
|
+
);
|
|
925
|
+
return [...baseColumns, ...jsonColumns].join(", ");
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private projectionAlias(idx: number) {
|
|
929
|
+
return `__akan_projection_${idx}`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private fromProjectedRow(row: ProjectedSqliteDocumentRow, fields: string[]) {
|
|
933
|
+
const doc: DocumentRecord = {
|
|
934
|
+
id: row.id,
|
|
935
|
+
createdAt: Number(row.createdAt),
|
|
936
|
+
updatedAt: Number(row.updatedAt),
|
|
937
|
+
removedAt: row.removedAt ? Number(row.removedAt) : undefined,
|
|
938
|
+
};
|
|
939
|
+
const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
|
|
940
|
+
for (const [idx, field] of jsonFields.entries()) {
|
|
941
|
+
const value = this.parseProjectedValue(row[this.projectionAlias(idx)]);
|
|
942
|
+
const props = (this.database.doc[FIELD_META] as unknown as FieldMap)[field]?.getProps?.();
|
|
943
|
+
doc[field] = props ? this.decodeFieldValue(value, props) : value;
|
|
944
|
+
}
|
|
945
|
+
return doc;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private parseProjectedValue(value: unknown) {
|
|
949
|
+
if (typeof value !== "string") return value;
|
|
950
|
+
const trimmed = value.trim();
|
|
951
|
+
if (!trimmed || (trimmed[0] !== "{" && trimmed[0] !== "[")) return value;
|
|
952
|
+
try {
|
|
953
|
+
return JSON.parse(trimmed);
|
|
954
|
+
} catch {
|
|
955
|
+
return value;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
904
959
|
private decodeDocumentPayload(payload: Record<string, unknown>) {
|
|
905
960
|
const fields = this.database.doc[FIELD_META] as unknown as FieldMap;
|
|
906
961
|
return Object.fromEntries(
|
|
@@ -1011,6 +1066,19 @@ export class SqliteDocumentStore {
|
|
|
1011
1066
|
return this.#insertStmt;
|
|
1012
1067
|
}
|
|
1013
1068
|
|
|
1069
|
+
private prepareReadStmt(sql: string) {
|
|
1070
|
+
const cached = this.#readStmtCache.get(sql);
|
|
1071
|
+
if (cached) return cached;
|
|
1072
|
+
|
|
1073
|
+
if (this.#readStmtCache.size >= 128) {
|
|
1074
|
+
const oldest = this.#readStmtCache.keys().next().value;
|
|
1075
|
+
if (oldest) this.#readStmtCache.delete(oldest);
|
|
1076
|
+
}
|
|
1077
|
+
const stmt = this.owner.getConnection().prepare(sql);
|
|
1078
|
+
this.#readStmtCache.set(sql, stmt);
|
|
1079
|
+
return stmt;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1014
1082
|
private assertValidRefName(refName: string) {
|
|
1015
1083
|
if (!REF_NAME_RE.test(refName) || RESERVED_RE.test(refName))
|
|
1016
1084
|
throw new Error(`Invalid database identifier: ${refName}`);
|
|
@@ -19,7 +19,7 @@ export interface SolidEnv extends BaseEnv {
|
|
|
19
19
|
solid?: SolidConfig;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export type SolidValueType = "string" | "number" | "buffer";
|
|
22
|
+
export type SolidValueType = "string" | "number" | "buffer" | "json";
|
|
23
23
|
|
|
24
24
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
25
|
|
|
@@ -62,19 +62,19 @@ export const openSolidDatabase = async (config: Required<SolidConfig>) => {
|
|
|
62
62
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
export const encodeSolidValue = (value:
|
|
65
|
+
export const encodeSolidValue = (value: unknown): { type: SolidValueType; value: string | Buffer } => {
|
|
66
66
|
if (Buffer.isBuffer(value)) return { type: "buffer", value };
|
|
67
67
|
if (typeof value === "number") return { type: "number", value: String(value) };
|
|
68
|
-
return { type: "string", value };
|
|
68
|
+
if (typeof value === "string") return { type: "string", value };
|
|
69
|
+
|
|
70
|
+
return { type: "json", value: JSON.stringify(value ?? null) };
|
|
69
71
|
};
|
|
70
72
|
|
|
71
|
-
export const decodeSolidValue = <T
|
|
72
|
-
type: SolidValueType,
|
|
73
|
-
value: string | Buffer | null,
|
|
74
|
-
): T | undefined => {
|
|
73
|
+
export const decodeSolidValue = <T>(type: SolidValueType, value: string | Buffer | null): T | undefined => {
|
|
75
74
|
if (value === null) return undefined;
|
|
76
75
|
if (type === "buffer") return Buffer.isBuffer(value) ? (value as T) : (Buffer.from(value) as T);
|
|
77
76
|
if (type === "number") return Number(value) as T;
|
|
77
|
+
if (type === "json") return JSON.parse(typeof value === "string" ? value : value.toString()) as T;
|
|
78
78
|
return String(value) as T;
|
|
79
79
|
};
|
|
80
80
|
|
|
@@ -16,6 +16,7 @@ export interface UploadRequest {
|
|
|
16
16
|
meta?: { [key: string]: string };
|
|
17
17
|
rename?: string;
|
|
18
18
|
host?: string;
|
|
19
|
+
access?: "public" | "private";
|
|
19
20
|
}
|
|
20
21
|
export interface CopyRequest {
|
|
21
22
|
bucket: string;
|
|
@@ -29,6 +30,7 @@ export interface UploadFromStreamRequest {
|
|
|
29
30
|
body: ReadableStream;
|
|
30
31
|
mimetype: string;
|
|
31
32
|
root?: string;
|
|
33
|
+
access?: "public" | "private";
|
|
32
34
|
updateProgress: (progress: { loaded?: number; total?: number; part?: number }) => void;
|
|
33
35
|
uploadSuccess: (url: string) => void;
|
|
34
36
|
}
|
|
@@ -47,17 +49,22 @@ export interface StorageAdaptor {
|
|
|
47
49
|
saveData(request: DownloadRequest): Promise<LocalFilePath>;
|
|
48
50
|
copyData(request: CopyRequest): Promise<string>;
|
|
49
51
|
deleteData(url: string): Promise<boolean>;
|
|
52
|
+
deleteDataByPath(path: string): Promise<boolean>;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export interface BlobStorageOptions extends BaseEnv {
|
|
53
|
-
blobStorage?: { baseDir?: string; urlPrefix?: string };
|
|
56
|
+
blobStorage?: { baseDir?: string; privateBaseDir?: string; urlPrefix?: string };
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export class BlobStorage
|
|
57
60
|
extends adapt("blobStorage", ({ env }) => ({
|
|
58
61
|
root: env(
|
|
59
62
|
({ appName, blobStorage = { baseDir: "local", urlPrefix: "/backend/localFile/getBlob" } }: BlobStorageOptions) =>
|
|
60
|
-
`${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.baseDir}/${appName}/backend`,
|
|
63
|
+
`${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.baseDir ?? "local"}/${appName}/backend`,
|
|
64
|
+
),
|
|
65
|
+
privateRoot: env(
|
|
66
|
+
({ appName, blobStorage = { privateBaseDir: "local" } }: BlobStorageOptions) =>
|
|
67
|
+
`${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.privateBaseDir ?? "local"}/${appName}/server-private`,
|
|
61
68
|
),
|
|
62
69
|
urlPrefix: env(
|
|
63
70
|
({ blobStorage = { urlPrefix: "/backend/localFile/getBlob" } }: BlobStorageOptions) => blobStorage.urlPrefix,
|
|
@@ -68,12 +75,15 @@ export class BlobStorage
|
|
|
68
75
|
#localPathToUrl(path: string) {
|
|
69
76
|
return `${this.urlPrefix}/${path}`;
|
|
70
77
|
}
|
|
78
|
+
#resolveFilePath(path: string) {
|
|
79
|
+
return path.startsWith("private/") ? `${this.privateRoot}/${path}` : `${this.root}/${path}`;
|
|
80
|
+
}
|
|
71
81
|
async readData(path: string): Promise<ReadableStream> {
|
|
72
|
-
const filePath =
|
|
82
|
+
const filePath = this.#resolveFilePath(path);
|
|
73
83
|
return Bun.file(filePath).stream();
|
|
74
84
|
}
|
|
75
85
|
async readDataAsJson<T>(path: string) {
|
|
76
|
-
const filePath =
|
|
86
|
+
const filePath = this.#resolveFilePath(path);
|
|
77
87
|
return Bun.file(filePath).json() as T;
|
|
78
88
|
}
|
|
79
89
|
async getDataList(prefix?: string) {
|
|
@@ -81,14 +91,21 @@ export class BlobStorage
|
|
|
81
91
|
const paths = Array.from(new Bun.Glob("*").scanSync({ cwd: dir, onlyFiles: false }));
|
|
82
92
|
return paths.map((path) => this.#localPathToUrl(path));
|
|
83
93
|
}
|
|
84
|
-
async uploadDataFromLocal({ path, localPath, meta }: UploadRequest) {
|
|
85
|
-
const filePath = `${this.root}/${path}`;
|
|
94
|
+
async uploadDataFromLocal({ path, localPath, meta, access = "public" }: UploadRequest) {
|
|
95
|
+
const filePath = access === "private" ? `${this.privateRoot}/${path}` : `${this.root}/${path}`;
|
|
86
96
|
await Bun.write(filePath, Bun.file(localPath));
|
|
87
97
|
if (meta) await Bun.write(`${filePath}.meta`, JSON.stringify(meta));
|
|
88
98
|
return this.#localPathToUrl(path);
|
|
89
99
|
}
|
|
90
|
-
async uploadDataFromStream({
|
|
91
|
-
|
|
100
|
+
async uploadDataFromStream({
|
|
101
|
+
path,
|
|
102
|
+
body,
|
|
103
|
+
mimetype,
|
|
104
|
+
updateProgress,
|
|
105
|
+
uploadSuccess,
|
|
106
|
+
access = "public",
|
|
107
|
+
}: UploadFromStreamRequest) {
|
|
108
|
+
const filePath = access === "private" ? `${this.privateRoot}/${path}` : `${this.root}/${path}`;
|
|
92
109
|
try {
|
|
93
110
|
await Bun.write(filePath, new Response(body));
|
|
94
111
|
uploadSuccess(this.#localPathToUrl(path));
|
|
@@ -106,12 +123,21 @@ export class BlobStorage
|
|
|
106
123
|
await Bun.write(`${this.root}/${pastePath}`, Bun.file(`${this.root}/${copyPath}`));
|
|
107
124
|
return pastePath;
|
|
108
125
|
}
|
|
126
|
+
async deleteDataByPath(path: string) {
|
|
127
|
+
try {
|
|
128
|
+
await Bun.file(this.#resolveFilePath(path)).delete();
|
|
129
|
+
return true;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
this.logger.error(error instanceof Error ? error.message : "Unknown error");
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
109
135
|
async deleteData(url: string) {
|
|
110
136
|
try {
|
|
111
137
|
const basePath = this.#localPathToUrl("");
|
|
112
138
|
if (!url.startsWith(basePath)) throw new Error("Invalid Base URL, Unable to delete data");
|
|
113
139
|
const path = url.replace(basePath, "");
|
|
114
|
-
await
|
|
140
|
+
await this.deleteDataByPath(path);
|
|
115
141
|
return true;
|
|
116
142
|
} catch (error) {
|
|
117
143
|
this.logger.error(error instanceof Error ? error.message : "Unknown error");
|
package/service/serviceModule.ts
CHANGED
|
@@ -45,12 +45,7 @@ export class ServiceModel<
|
|
|
45
45
|
this.cnst,
|
|
46
46
|
this.db,
|
|
47
47
|
Object.assign({}, this.srvMap, ...srvs.map((srv) => srv.srvMap)),
|
|
48
|
-
) as unknown as ServiceModel<
|
|
49
|
-
Srv & MergeAllKeyOfObjects<SrvModules, "srv">,
|
|
50
|
-
CnstModel,
|
|
51
|
-
DbModel,
|
|
52
|
-
SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">
|
|
53
|
-
>;
|
|
48
|
+
) as unknown as ServiceModel<Srv, CnstModel, DbModel, SrvMap & MergeAllKeyOfObjects<SrvModules, "srvMap">>;
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
static getDefaultDbServiceMethods(className: string) {
|
package/signal/base.signal.ts
CHANGED
|
@@ -31,7 +31,7 @@ export class BaseEndpoint extends endpoint(srv.base, ({ query, mutation, message
|
|
|
31
31
|
})) {}
|
|
32
32
|
|
|
33
33
|
export class Base extends serverSignal(BaseEndpoint, BaseInternal) {}
|
|
34
|
-
export const base = SignalRegistry.registerService(BaseInternal, BaseEndpoint, Base);
|
|
34
|
+
export const base = SignalRegistry.registerService("base" as const, BaseInternal, BaseEndpoint, Base);
|
|
35
35
|
|
|
36
36
|
const createBaseFetch = () => FetchClient.from(base);
|
|
37
37
|
type BaseFetch = ReturnType<typeof createBaseFetch>;
|
package/signal/index.ts
CHANGED
package/signal/middleware.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { BaseEnv, Cls, PromiseOrObject } from "akanjs/base";
|
|
|
2
2
|
import { type CacheAdaptor, CacheAdaptorRole } from "akanjs/service";
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
4
|
import type { SignalContext } from "./signalContext";
|
|
5
|
+
import { traceCache } from "./trace";
|
|
5
6
|
|
|
6
7
|
export interface Middleware<Env extends BaseEnv = BaseEnv> {
|
|
7
8
|
use(env: Env): PromiseOrObject<(context: SignalContext, next: () => Promise<unknown>) => PromiseOrObject<unknown>>;
|
|
@@ -52,12 +53,15 @@ export class Cache extends middleware("cache") {
|
|
|
52
53
|
if (cached) {
|
|
53
54
|
context.adaptor.logger.debug(`Cache hit ${context.key}`);
|
|
54
55
|
try {
|
|
55
|
-
|
|
56
|
+
const parsed = JSON.parse(cached);
|
|
57
|
+
traceCache(true);
|
|
58
|
+
return parsed;
|
|
56
59
|
} catch (parseError) {
|
|
57
60
|
context.adaptor.logger.warn(`Cache parse error ${context.key}: ${String(parseError)}`);
|
|
58
61
|
await cache.delete(topic, key);
|
|
59
62
|
}
|
|
60
63
|
}
|
|
64
|
+
traceCache(false);
|
|
61
65
|
|
|
62
66
|
const result = await next();
|
|
63
67
|
|
package/signal/signalContext.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { Adaptor, AdaptorCls, DatabaseService, InjectRegistry, LiveRegistry
|
|
|
18
18
|
import type { Internal, InternalCls, InternalInfo, MiddlewareCls } from ".";
|
|
19
19
|
import type { EndpointInfo } from "./endpointInfo";
|
|
20
20
|
import { Exception } from "./exception";
|
|
21
|
+
import { isTraceEnabled, runWithTrace, SignalTrace, traceSpan } from "./trace";
|
|
21
22
|
|
|
22
23
|
export type SignalTransportType = "http" | "websocket";
|
|
23
24
|
|
|
@@ -54,6 +55,7 @@ export class SignalContext<
|
|
|
54
55
|
adaptor: Adaptor;
|
|
55
56
|
args: unknown[] = [];
|
|
56
57
|
internalArgs: unknown[] = [];
|
|
58
|
+
trace: SignalTrace | null = null;
|
|
57
59
|
#registry: InjectRegistry;
|
|
58
60
|
#env: Env;
|
|
59
61
|
#live: LiveRegistry;
|
|
@@ -87,6 +89,7 @@ export class SignalContext<
|
|
|
87
89
|
this.#env = env;
|
|
88
90
|
this.#live = live;
|
|
89
91
|
this.#middleware = middleware;
|
|
92
|
+
if (isTraceEnabled()) this.trace = new SignalTrace(key, endpointInfo.type);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
getAdaptor<T extends Adaptor>(adaptorCls: AdaptorCls<T>): T {
|
|
@@ -97,11 +100,18 @@ export class SignalContext<
|
|
|
97
100
|
return instance as T;
|
|
98
101
|
}
|
|
99
102
|
async init() {
|
|
100
|
-
|
|
103
|
+
if (this.trace) {
|
|
104
|
+
const start = performance.now();
|
|
105
|
+
this.args = await this.ctx.getArgs(this.endpointInfo);
|
|
106
|
+
this.trace.recordSpan("argParse", performance.now() - start);
|
|
107
|
+
} else {
|
|
108
|
+
this.args = await this.ctx.getArgs(this.endpointInfo);
|
|
109
|
+
}
|
|
101
110
|
return this;
|
|
102
111
|
}
|
|
103
112
|
async #checkGuards() {
|
|
104
113
|
const guards = this.endpointInfo.signalOption.guards ?? [];
|
|
114
|
+
if (guards.length === 0) return;
|
|
105
115
|
for (const GuardCls of guards) {
|
|
106
116
|
const guard = new GuardCls();
|
|
107
117
|
const canPass = guard.canPass(this);
|
|
@@ -109,41 +119,72 @@ export class SignalContext<
|
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
async exec() {
|
|
122
|
+
if (!this.trace) return await this.#exec();
|
|
123
|
+
return await runWithTrace(this.trace, async () => {
|
|
124
|
+
try {
|
|
125
|
+
return await this.#exec();
|
|
126
|
+
} finally {
|
|
127
|
+
this.trace?.finalize();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async #exec() {
|
|
112
132
|
if (!this.endpointInfo.execFn) throw new Exception.Error("Exec function is not set");
|
|
113
|
-
const
|
|
133
|
+
const endpointMiddlewares = this.endpointInfo.signalOption.middlewares ?? [];
|
|
114
134
|
const coreExec = async () => {
|
|
115
135
|
if (!this.endpointInfo.execFn) throw new Exception.Error("Exec function is not set");
|
|
116
|
-
await this.#checkGuards();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
if (this.trace) await traceSpan("guards", () => this.#checkGuards());
|
|
137
|
+
else await this.#checkGuards();
|
|
138
|
+
if (this.endpointInfo.internalArgs.length > 0) {
|
|
139
|
+
this.internalArgs = await Promise.all(
|
|
140
|
+
this.endpointInfo.internalArgs.map((arg) => {
|
|
141
|
+
const argValue = new arg.argRef().getArg(this) ?? null;
|
|
142
|
+
if (argValue === null && !arg.option?.nullable)
|
|
143
|
+
throw new Exception.Unauthorized(`Internal Argument ${arg.argRef.name} is required`);
|
|
144
|
+
return argValue;
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (!this.trace) return await this.endpointInfo.execFn.call(this.adaptor, ...this.args, ...this.internalArgs);
|
|
149
|
+
return await traceSpan(
|
|
150
|
+
"handler",
|
|
151
|
+
async () => await this.endpointInfo.execFn?.call(this.adaptor, ...this.args, ...this.internalArgs),
|
|
124
152
|
);
|
|
125
|
-
const result = await this.endpointInfo.execFn.call(this.adaptor, ...this.args, ...this.internalArgs);
|
|
126
|
-
return result;
|
|
127
153
|
};
|
|
128
154
|
let next = coreExec;
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
if (this.#middleware.size > 0 || endpointMiddlewares.length > 0) {
|
|
156
|
+
const middlewares = [...this.#middleware.values(), ...endpointMiddlewares];
|
|
157
|
+
for (let i = middlewares.length - 1; i >= 0; i--) {
|
|
158
|
+
const MiddlewareCls = middlewares[i];
|
|
159
|
+
if (!MiddlewareCls) continue;
|
|
160
|
+
const middleware = new MiddlewareCls();
|
|
161
|
+
const currentNext = next;
|
|
162
|
+
next = async () => await (await middleware.use(this.getEnv()))(this, currentNext);
|
|
163
|
+
}
|
|
135
164
|
}
|
|
136
|
-
const result = await next();
|
|
165
|
+
const result = this.trace ? await traceSpan("execChain", () => next()) : await next();
|
|
137
166
|
if (this.endpointInfo.type === "pubsub") return;
|
|
138
167
|
if (result instanceof Response) return result;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
if (!this.trace) {
|
|
169
|
+
const resolved = await SignalContext.resolveReturn(result, {
|
|
170
|
+
signalContext: this,
|
|
171
|
+
returnRef: this.endpointInfo.returns.returnRef,
|
|
172
|
+
arrDepth: this.endpointInfo.returns.arrDepth,
|
|
173
|
+
registry: this.#registry,
|
|
174
|
+
live: this.#live,
|
|
175
|
+
});
|
|
176
|
+
return this.ctx.makeResponse(resolved, this.endpointInfo);
|
|
177
|
+
}
|
|
178
|
+
const resolved = await traceSpan("resolveReturn", () =>
|
|
179
|
+
SignalContext.resolveReturn(result, {
|
|
180
|
+
signalContext: this,
|
|
181
|
+
returnRef: this.endpointInfo.returns.returnRef,
|
|
182
|
+
arrDepth: this.endpointInfo.returns.arrDepth,
|
|
183
|
+
registry: this.#registry,
|
|
184
|
+
live: this.#live,
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
return await traceSpan("serialize", async () => this.ctx.makeResponse(resolved, this.endpointInfo));
|
|
147
188
|
}
|
|
148
189
|
static async try(
|
|
149
190
|
endpoint: Adaptor,
|
|
@@ -257,6 +298,11 @@ export class SignalContext<
|
|
|
257
298
|
service: DatabaseService,
|
|
258
299
|
{ arrDepth, nullable }: { arrDepth: number; nullable: boolean },
|
|
259
300
|
): Promise<unknown> {
|
|
301
|
+
if (value === null || value === undefined) {
|
|
302
|
+
if (nullable) return null;
|
|
303
|
+
throw new Error(`Document ${value} is not found`);
|
|
304
|
+
}
|
|
305
|
+
if (arrDepth > 0 && Array.isArray(value) && value.length === 0) return [];
|
|
260
306
|
if (arrDepth === 0)
|
|
261
307
|
return await service.__load(String(value)).then((doc) => {
|
|
262
308
|
if (doc === null) {
|
|
@@ -300,19 +346,24 @@ export class SignalContext<
|
|
|
300
346
|
export class HttpExecutionContext<Appended = unknown> {
|
|
301
347
|
req: Bun.BunRequest & Appended;
|
|
302
348
|
res = Response;
|
|
303
|
-
url: URL;
|
|
349
|
+
#url: URL | null = null;
|
|
304
350
|
params: RuntimeRecord = {};
|
|
305
351
|
searchParams: RuntimeRecord = {};
|
|
306
352
|
body: RuntimeRecord = {};
|
|
307
353
|
constructor(req: Bun.BunRequest) {
|
|
308
354
|
this.req = req as Bun.BunRequest & Appended;
|
|
309
|
-
|
|
355
|
+
}
|
|
356
|
+
get url() {
|
|
357
|
+
if (!this.#url) this.#url = new URL(this.req.url);
|
|
358
|
+
return this.#url;
|
|
310
359
|
}
|
|
311
360
|
async getArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
361
|
+
if (endpointInfo.args.length === 0) return [];
|
|
312
362
|
this.params = this.req.params;
|
|
313
363
|
|
|
314
|
-
const
|
|
315
|
-
|
|
364
|
+
const hasBodyArgs = endpointInfo.args.some((arg) => arg.type === "body");
|
|
365
|
+
const hasUploadArgs = hasBodyArgs && endpointInfo.args.some((arg) => arg.type === "body" && arg.argRef === Upload);
|
|
366
|
+
if (endpointInfo.type === "mutation" && hasBodyArgs && this.req.body) {
|
|
316
367
|
if (hasUploadArgs) {
|
|
317
368
|
const formData = await this.req.formData();
|
|
318
369
|
this.body = {};
|
|
@@ -361,6 +412,9 @@ export class HttpExecutionContext<Appended = unknown> {
|
|
|
361
412
|
}
|
|
362
413
|
makeResponse(result: unknown, endpointInfo: EndpointInfo) {
|
|
363
414
|
if (result instanceof Response) return result;
|
|
415
|
+
if (endpointInfo.returns.arrDepth === 0 && PrimitiveRegistry.has(endpointInfo.returns.returnRef as Cls)) {
|
|
416
|
+
return this.res.json(result);
|
|
417
|
+
}
|
|
364
418
|
const value = serialize(endpointInfo.returns.returnRef, endpointInfo.returns.arrDepth, result, "object", {
|
|
365
419
|
nullable: endpointInfo.returns.nullable,
|
|
366
420
|
});
|