akanjs 3.1.1-rc.4 → 3.1.1-rc.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/document/by.ts +4 -3
- package/document/into.ts +2 -2
- package/package.json +1 -1
- package/server/akanApp.ts +2 -0
- package/server/resolver/database.resolver.ts +4 -3
- package/server/routing/apiRouter.ts +2 -0
- package/server/routing/wsUpgrade.ts +10 -0
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +10 -9
- package/service/predefinedAdaptor/sql/types.ts +1 -1
- package/types/document/by.d.ts +4 -3
- package/types/document/into.d.ts +2 -2
- package/types/server/routing/wsUpgrade.d.ts +4 -0
- package/types/service/predefinedAdaptor/sql/types.d.ts +1 -1
package/document/by.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Cls, FIELD_META, type GetActionObject } from "akanjs/base";
|
|
2
2
|
import { applyMixins } from "akanjs/common";
|
|
3
3
|
import { ConstantRegistry, type DocumentConstantModelRef, type DocumentModel, type FieldObject } from "akanjs/constant";
|
|
4
|
+
import type { DocumentTouchOptions } from "./documentQuery";
|
|
4
5
|
|
|
5
6
|
export type DatabaseCls<Schema = unknown> = Cls<Schema, { refName: string; [FIELD_META]: FieldObject }>;
|
|
6
7
|
export type DatabaseInstanceOf<DatabaseRef extends DatabaseCls> = DatabaseRef extends new (
|
|
@@ -14,7 +15,7 @@ export interface DefaultDocMtds<TDocument> {
|
|
|
14
15
|
refresh(): Promise<this>;
|
|
15
16
|
isModified(field?: keyof TDocument & string): boolean;
|
|
16
17
|
set(data: Partial<TDocument>): this;
|
|
17
|
-
save(): Promise<this>;
|
|
18
|
+
save(options?: DocumentTouchOptions): Promise<this>;
|
|
18
19
|
toJSON(): DocumentModel<TDocument>;
|
|
19
20
|
toObject(): DocumentModel<TDocument>;
|
|
20
21
|
}
|
|
@@ -25,7 +26,7 @@ type HydratedDocumentMethods<TDocument, Self> = Omit<
|
|
|
25
26
|
refresh(): Promise<Self>;
|
|
26
27
|
isModified(field?: keyof TDocument & string): boolean;
|
|
27
28
|
set(data: Partial<TDocument>): Self;
|
|
28
|
-
save(): Promise<Self>;
|
|
29
|
+
save(options?: DocumentTouchOptions): Promise<Self>;
|
|
29
30
|
};
|
|
30
31
|
type HydratedDocumentWithId<TDocument> = TDocument & { id: string } & HydratedDocumentMethods<TDocument, TDocument>;
|
|
31
32
|
interface ChainableHydratedMethods<TDocument>
|
|
@@ -33,7 +34,7 @@ interface ChainableHydratedMethods<TDocument>
|
|
|
33
34
|
refresh(): Promise<this>;
|
|
34
35
|
isModified(field?: keyof TDocument & string): boolean;
|
|
35
36
|
set(data: Partial<TDocument>): this;
|
|
36
|
-
save(): Promise<this>;
|
|
37
|
+
save(options?: DocumentTouchOptions): Promise<this>;
|
|
37
38
|
}
|
|
38
39
|
type ChainableHydratedDocument<TDocument> = TDocument & {
|
|
39
40
|
id: string;
|
package/document/into.ts
CHANGED
|
@@ -25,8 +25,8 @@ interface DefaultMdlStats<
|
|
|
25
25
|
_FilterQuery extends DocumentQuery<TSchema> = DocumentQuery<TSchema>,
|
|
26
26
|
_Projection = DocumentProjection<TSchema>,
|
|
27
27
|
> {
|
|
28
|
-
pickOneAndWrite: (query: _FilterQuery, rawData: _Partial) => Promise<TDocument>;
|
|
29
|
-
pickAndWrite: (docId: string, rawData: _Partial) => Promise<TDocument>;
|
|
28
|
+
pickOneAndWrite: (query: _FilterQuery, rawData: _Partial, options?: DocumentTouchOptions) => Promise<TDocument>;
|
|
29
|
+
pickAndWrite: (docId: string, rawData: _Partial, options?: DocumentTouchOptions) => Promise<TDocument>;
|
|
30
30
|
pickOne: (query: _FilterQuery, projection?: _Projection) => Promise<TDocument>;
|
|
31
31
|
pickById: (docId: string | undefined, projection?: _Projection) => Promise<TDocument>;
|
|
32
32
|
sample: (query: _FilterQuery, size?: number) => Promise<TDocument[]>;
|
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { OpsRoute } from "./ops/opsRoute";
|
|
|
24
24
|
import { StatusAccess } from "./ops/statusAccess";
|
|
25
25
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
26
26
|
import { HostAllowlist } from "./routing/hostAllowlist";
|
|
27
|
+
import { WsUpgrade } from "./routing/wsUpgrade";
|
|
27
28
|
import { SelfExec } from "./selfExec";
|
|
28
29
|
import { resolveStaticPath } from "./staticPath";
|
|
29
30
|
import { getWebConfigFromEnv } from "./types";
|
|
@@ -670,6 +671,7 @@ export class AkanApp {
|
|
|
670
671
|
} catch {
|
|
671
672
|
return new Response("Forbidden", { status: 403 });
|
|
672
673
|
}
|
|
674
|
+
if (!WsUpgrade.isRequested(req)) return WsUpgrade.required();
|
|
673
675
|
const upstreamWs = new WebSocket(`ws://${upstream.host}:${upstream.port}${url.pathname}${url.search}`, {
|
|
674
676
|
headers: makeAkanChildProxyHeaders(req, child.idx, server.requestIP(req)),
|
|
675
677
|
} as unknown as string[]);
|
|
@@ -278,9 +278,10 @@ export class DatabaseResolver {
|
|
|
278
278
|
timedQuery(() => store.pickOne(query, { select: projection })),
|
|
279
279
|
pickById,
|
|
280
280
|
|
|
281
|
-
pickAndWrite: async (id: string, rawData: any) =>
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
pickAndWrite: async (id: string, rawData: any, options?: DocumentTouchOptions) =>
|
|
282
|
+
await (await pickById(id)).set(rawData).save(...touchArgs(options)),
|
|
283
|
+
pickOneAndWrite: async (query: QueryOf<any>, rawData: any, options?: DocumentTouchOptions) =>
|
|
284
|
+
await (await timedQuery(() => store.pickOne(query))).set(rawData).save(...touchArgs(options)),
|
|
284
285
|
exists: async (query: QueryOf<any>, options?: DocumentScopeOptions) =>
|
|
285
286
|
await timedQuery(() => store.exists(query, ...scopeArgs(options))),
|
|
286
287
|
sample: (query: QueryOf<any>, size = 1) => timedQuery(() => store.find(query, { sample: size, limit: size })),
|
|
@@ -15,6 +15,7 @@ import { SignalResolver } from "../resolver";
|
|
|
15
15
|
import type { HttpRoutes, SignalRouteOptions, WebsocketRoutes } from "../types";
|
|
16
16
|
import { AppWsData } from "./appWsData";
|
|
17
17
|
import type { HostAllowlist } from "./hostAllowlist";
|
|
18
|
+
import { WsUpgrade } from "./wsUpgrade";
|
|
18
19
|
|
|
19
20
|
export interface HmrStateSource {
|
|
20
21
|
readonly state: {
|
|
@@ -93,6 +94,7 @@ export class ApiRouter {
|
|
|
93
94
|
}
|
|
94
95
|
const upgraded = upgradeAppWs(req, AppWsData.fromRequest(req));
|
|
95
96
|
if (upgraded) return;
|
|
97
|
+
if (!WsUpgrade.isRequested(req)) return WsUpgrade.required();
|
|
96
98
|
return new Response("Failed to upgrade to WebSocket", { status: 500 });
|
|
97
99
|
},
|
|
98
100
|
...Object.fromEntries(endpointEntries),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
export class WsUpgrade {
|
|
3
|
+
static isRequested(req: Request) {
|
|
4
|
+
return req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
static required() {
|
|
8
|
+
return new Response("Upgrade Required", { status: 426, headers: { Upgrade: "websocket", Connection: "Upgrade" } });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -412,7 +412,7 @@ export class SqlDocumentStore {
|
|
|
412
412
|
|
|
413
413
|
async update(id: string, patch: DocumentRecord, options: WriteHookOptions = {}) {
|
|
414
414
|
const current = await this.pickByIdForWrite(id);
|
|
415
|
-
return await this.writeUpdatedDocument(id, { ...current, ...patch, id
|
|
415
|
+
return await this.writeUpdatedDocument(id, { ...current, ...patch, id }, current, options);
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
async remove(id: string) {
|
|
@@ -957,9 +957,9 @@ export class SqlDocumentStore {
|
|
|
957
957
|
id: string,
|
|
958
958
|
data: DocumentRecord,
|
|
959
959
|
originalData: DocumentRecord,
|
|
960
|
-
{ runSaveHooks = true, crudType = "update" }: WriteHookOptions = {},
|
|
960
|
+
{ runSaveHooks = true, crudType = "update", touch = true }: WriteHookOptions = {},
|
|
961
961
|
) {
|
|
962
|
-
const prepared = this.prepareDocument({ ...data, id, updatedAt: dayjs() });
|
|
962
|
+
const prepared = this.prepareDocument({ ...data, id, updatedAt: touch ? dayjs() : originalData.updatedAt });
|
|
963
963
|
this.#assertImmutableUnchanged(prepared, originalData);
|
|
964
964
|
const doc = this.hydrate(prepared, originalData);
|
|
965
965
|
|
|
@@ -968,7 +968,7 @@ export class SqlDocumentStore {
|
|
|
968
968
|
await this.runHooks(crudType, crudType, doc, "pre", previous);
|
|
969
969
|
const row = this.toRow(doc);
|
|
970
970
|
const stored = (originalData as Record<symbol, StoredRow | undefined>)[SqlDocumentStore.#storedRow];
|
|
971
|
-
await this.#writeChanges(id, stored ?? this.toRow(originalData), row);
|
|
971
|
+
await this.#writeChanges(id, stored ?? this.toRow(originalData), row, touch);
|
|
972
972
|
await this.runHooks(crudType, crudType, doc, "post", previous);
|
|
973
973
|
if (runSaveHooks) await this.runHooks("save", crudType, doc, "post", previous);
|
|
974
974
|
return this.#withStoredRow(doc, row);
|
|
@@ -978,11 +978,12 @@ export class SqlDocumentStore {
|
|
|
978
978
|
* Merges only the fields that changed into the stored `_doc`, so a concurrent write to another field survives. A key
|
|
979
979
|
* the stored row lacks is written out, which is what `q.missing` tells rows from before a field existed apart by.
|
|
980
980
|
*/
|
|
981
|
-
async #writeChanges(id: string, read: StoredRow, written: StoredRow) {
|
|
981
|
+
async #writeChanges(id: string, read: StoredRow, written: StoredRow, touch: boolean) {
|
|
982
982
|
const before = SqlDocumentStore.#encodedFields(read);
|
|
983
983
|
const after = SqlDocumentStore.#encodedFields(written);
|
|
984
|
-
|
|
985
|
-
const
|
|
984
|
+
|
|
985
|
+
const assignments = [touch ? `"updatedAt" = ?` : `"updatedAt" = "updatedAt"`];
|
|
986
|
+
const params: unknown[] = touch ? [written.updatedAt] : [];
|
|
986
987
|
for (const column of ["createdAt", "removedAt"] as const) {
|
|
987
988
|
if (before.get(column) === after.get(column)) continue;
|
|
988
989
|
assignments.push(`${quoteIdent(column)} = ?`);
|
|
@@ -1201,10 +1202,10 @@ export class SqlDocumentStore {
|
|
|
1201
1202
|
},
|
|
1202
1203
|
},
|
|
1203
1204
|
save: {
|
|
1204
|
-
async value(this: DocumentRecord) {
|
|
1205
|
+
async value(this: DocumentRecord, { touch }: DocumentTouchOptions = {}) {
|
|
1205
1206
|
if (!this.id) return store.create(this);
|
|
1206
1207
|
store.#assertLiveRow(this);
|
|
1207
|
-
return store.update(this.id as string, store.#changesOf(this));
|
|
1208
|
+
return store.update(this.id as string, store.#changesOf(this), touch === false ? { touch } : {});
|
|
1208
1209
|
},
|
|
1209
1210
|
},
|
|
1210
1211
|
refresh: {
|
|
@@ -170,7 +170,7 @@ export type FindOneOptions = DocumentScopeOptions & {
|
|
|
170
170
|
select?: ProjectionOption;
|
|
171
171
|
};
|
|
172
172
|
export type FindIdOptions = DocumentScopeOptions & { sort?: SortOption; skip?: number | null; sample?: boolean };
|
|
173
|
-
export type WriteHookOptions = { runSaveHooks?: boolean; crudType?: "update" | "remove" };
|
|
173
|
+
export type WriteHookOptions = DocumentTouchOptions & { runSaveHooks?: boolean; crudType?: "update" | "remove" };
|
|
174
174
|
export type QueryOperatorName = Extract<DocumentQueryNode, { kind: "op" }>["op"];
|
|
175
175
|
export interface SqliteDocumentRow {
|
|
176
176
|
id: string;
|
package/types/document/by.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Cls, FIELD_META, type GetActionObject } from "akanjs/base";
|
|
2
2
|
import { type DocumentConstantModelRef, type DocumentModel, type FieldObject } from "akanjs/constant";
|
|
3
|
+
import type { DocumentTouchOptions } from "./documentQuery.d.ts";
|
|
3
4
|
export type DatabaseCls<Schema = unknown> = Cls<Schema, {
|
|
4
5
|
refName: string;
|
|
5
6
|
[FIELD_META]: FieldObject;
|
|
@@ -10,7 +11,7 @@ export interface DefaultDocMtds<TDocument> {
|
|
|
10
11
|
refresh(): Promise<this>;
|
|
11
12
|
isModified(field?: keyof TDocument & string): boolean;
|
|
12
13
|
set(data: Partial<TDocument>): this;
|
|
13
|
-
save(): Promise<this>;
|
|
14
|
+
save(options?: DocumentTouchOptions): Promise<this>;
|
|
14
15
|
toJSON(): DocumentModel<TDocument>;
|
|
15
16
|
toObject(): DocumentModel<TDocument>;
|
|
16
17
|
}
|
|
@@ -18,7 +19,7 @@ type HydratedDocumentMethods<TDocument, Self> = Omit<DefaultDocMtds<TDocument>,
|
|
|
18
19
|
refresh(): Promise<Self>;
|
|
19
20
|
isModified(field?: keyof TDocument & string): boolean;
|
|
20
21
|
set(data: Partial<TDocument>): Self;
|
|
21
|
-
save(): Promise<Self>;
|
|
22
|
+
save(options?: DocumentTouchOptions): Promise<Self>;
|
|
22
23
|
};
|
|
23
24
|
type HydratedDocumentWithId<TDocument> = TDocument & {
|
|
24
25
|
id: string;
|
|
@@ -27,7 +28,7 @@ interface ChainableHydratedMethods<TDocument> extends Omit<DefaultDocMtds<TDocum
|
|
|
27
28
|
refresh(): Promise<this>;
|
|
28
29
|
isModified(field?: keyof TDocument & string): boolean;
|
|
29
30
|
set(data: Partial<TDocument>): this;
|
|
30
|
-
save(): Promise<this>;
|
|
31
|
+
save(options?: DocumentTouchOptions): Promise<this>;
|
|
31
32
|
}
|
|
32
33
|
type ChainableHydratedDocument<TDocument> = TDocument & {
|
|
33
34
|
id: string;
|
package/types/document/into.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export type CRUDEventType = "create" | "update" | "remove";
|
|
|
10
10
|
export type SaveEventType = "save" | CRUDEventType;
|
|
11
11
|
export type SaveEventListener<Doc> = (doc: Doc, type: CRUDEventType, previous?: Doc) => PromiseOrObject<void>;
|
|
12
12
|
interface DefaultMdlStats<TDocument, TSchema, _Partial extends Partial<TSchema> = Partial<TSchema>, _FilterQuery extends DocumentQuery<TSchema> = DocumentQuery<TSchema>, _Projection = DocumentProjection<TSchema>> {
|
|
13
|
-
pickOneAndWrite: (query: _FilterQuery, rawData: _Partial) => Promise<TDocument>;
|
|
14
|
-
pickAndWrite: (docId: string, rawData: _Partial) => Promise<TDocument>;
|
|
13
|
+
pickOneAndWrite: (query: _FilterQuery, rawData: _Partial, options?: DocumentTouchOptions) => Promise<TDocument>;
|
|
14
|
+
pickAndWrite: (docId: string, rawData: _Partial, options?: DocumentTouchOptions) => Promise<TDocument>;
|
|
15
15
|
pickOne: (query: _FilterQuery, projection?: _Projection) => Promise<TDocument>;
|
|
16
16
|
pickById: (docId: string | undefined, projection?: _Projection) => Promise<TDocument>;
|
|
17
17
|
sample: (query: _FilterQuery, size?: number) => Promise<TDocument[]>;
|
|
@@ -170,7 +170,7 @@ export type FindIdOptions = DocumentScopeOptions & {
|
|
|
170
170
|
skip?: number | null;
|
|
171
171
|
sample?: boolean;
|
|
172
172
|
};
|
|
173
|
-
export type WriteHookOptions = {
|
|
173
|
+
export type WriteHookOptions = DocumentTouchOptions & {
|
|
174
174
|
runSaveHooks?: boolean;
|
|
175
175
|
crudType?: "update" | "remove";
|
|
176
176
|
};
|