akanjs 2.4.1 → 2.4.2-rc.1
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/client/csrTypes.ts +7 -0
- package/client/frameConfig.ts +6 -1
- package/client/rscNavigation.ts +9 -0
- package/common/index.ts +5 -0
- package/common/websocketAuth.ts +24 -0
- package/constant/fieldInfo.ts +4 -3
- package/constant/index.ts +2 -0
- package/constant/textFieldPathSet.ts +8 -0
- package/constant/textFieldPaths.ts +59 -0
- package/constant/types.ts +0 -4
- package/constant/via.ts +13 -28
- package/dictionary/dictInfo.ts +4 -0
- package/document/documentQuery.ts +17 -1
- package/document/documentSchema.ts +1 -15
- package/document/filterMeta.ts +4 -2
- package/fetch/client/fetchClient.ts +1 -0
- package/fetch/client/wsClient.ts +28 -1
- package/index.ts +6 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
- package/package.json +1 -1
- package/server/akanServer.ts +5 -4
- package/server/devtools/types.ts +2 -2
- package/server/resolver/database.resolver.ts +5 -6
- package/server/resolver/signal.resolver.ts +23 -0
- package/server/routing/apiRouter.ts +11 -3
- package/server/routing/appWsData.ts +50 -0
- package/server/rscClient.tsx +9 -0
- package/service/predefinedAdaptor/database.adaptor.ts +212 -88
- package/service/predefinedAdaptor/index.ts +1 -0
- package/service/predefinedAdaptor/searchIndex.ts +517 -0
- package/service/predefinedAdaptor/sqlDescriptor.ts +25 -0
- package/signal/signalContext.ts +37 -12
- package/signal/types.ts +2 -1
- package/types/client/csrTypes.d.ts +7 -0
- package/types/client/rscNavigation.d.ts +8 -0
- package/types/common/index.d.ts +1 -0
- package/types/common/websocketAuth.d.ts +19 -0
- package/types/constant/fieldInfo.d.ts +4 -3
- package/types/constant/index.d.ts +2 -0
- package/types/constant/textFieldPathSet.d.ts +8 -0
- package/types/constant/textFieldPaths.d.ts +10 -0
- package/types/constant/types.d.ts +0 -3
- package/types/constant/via.d.ts +4 -24
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/documentQuery.d.ts +13 -1
- package/types/document/documentSchema.d.ts +0 -3
- package/types/document/filterMeta.d.ts +2 -1
- package/types/fetch/client/wsClient.d.ts +6 -0
- package/types/index.d.ts +6 -0
- package/types/server/devtools/types.d.ts +2 -2
- package/types/server/resolver/signal.resolver.d.ts +6 -0
- package/types/server/routing/apiRouter.d.ts +3 -4
- package/types/server/routing/appWsData.d.ts +24 -0
- package/types/server/rscClient.d.ts +1 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +46 -13
- package/types/service/predefinedAdaptor/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/searchIndex.d.ts +73 -0
- package/types/service/predefinedAdaptor/sqlDescriptor.d.ts +5 -0
- package/types/signal/signalContext.d.ts +7 -0
- package/types/signal/types.d.ts +5 -1
- package/types/ui/Constant/schemaDoc.d.ts +2 -2
- package/ui/Constant/schemaDoc.ts +8 -2
- package/ui/Model/EditModal.tsx +24 -6
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type ConstantModel, textFieldRoles } from "akanjs/constant";
|
|
2
|
+
import { type DatabaseModel, type SearchColumn } from "akanjs/document";
|
|
3
|
+
import type { AkanSqlClient } from "./database.adaptor";
|
|
4
|
+
export declare const DOC_TABLE = "search_doc";
|
|
5
|
+
export declare const FTS_TABLE = "search_fts";
|
|
6
|
+
export declare const DEFAULT_TOKENIZER = "unicode61 remove_diacritics 2";
|
|
7
|
+
export declare const OPTIMIZE_CRON_KEY = "searchIndexOptimize";
|
|
8
|
+
export declare const OPTIMIZE_CRON = "17 4 * * *";
|
|
9
|
+
export declare const RETRY_INTERVAL_KEY = "searchIndexRetry";
|
|
10
|
+
export declare const RETRY_INTERVAL_MS = 60000;
|
|
11
|
+
export declare const DEFAULT_SEARCH_WEIGHTS: number[];
|
|
12
|
+
export interface SearchIndexOwner {
|
|
13
|
+
getConnection(): AkanSqlClient;
|
|
14
|
+
getMeta(key: string): Promise<string | undefined> | string | undefined;
|
|
15
|
+
setMeta(key: string, value: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface SearchIndexOptions {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
tokenizer: string;
|
|
20
|
+
}
|
|
21
|
+
type SearchColumns = Record<(typeof textFieldRoles)[number], string>;
|
|
22
|
+
/**
|
|
23
|
+
* Reads `AKAN_SEARCH_ENABLED`. Unset means enabled; an unrecognised value fails the boot rather than
|
|
24
|
+
* silently falling back, because a typo like `ture` would otherwise look identical to the default.
|
|
25
|
+
*/
|
|
26
|
+
export declare const parseSearchEnabled: (value: string | undefined) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Turns raw user input into an fts5 MATCH expression.
|
|
29
|
+
*
|
|
30
|
+
* Raw text cannot be passed through: `-`, `:`, `*`, `"` and a trailing `AND` are all fts5 syntax and each
|
|
31
|
+
* raises `SQLiteError` instead of returning no rows. Quoting every term makes the whole input literal.
|
|
32
|
+
*/
|
|
33
|
+
export declare const toMatchExpression: (text: string, { prefix, columns }?: {
|
|
34
|
+
prefix?: boolean;
|
|
35
|
+
columns?: readonly SearchColumn[];
|
|
36
|
+
}) => string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Owns the `search_doc` mirror and its fts5 index.
|
|
39
|
+
*
|
|
40
|
+
* The mirror is maintained by SQL triggers rather than document hooks because `updateOneByQuery` and friends
|
|
41
|
+
* deliberately fire no hooks — most searchable-field mutations go through exactly that path, so an app-level
|
|
42
|
+
* hook would miss them silently.
|
|
43
|
+
*/
|
|
44
|
+
export declare class SearchIndex {
|
|
45
|
+
#private;
|
|
46
|
+
constructor(owner: SearchIndexOwner, { enabled, tokenizer }: SearchIndexOptions);
|
|
47
|
+
get enabled(): boolean;
|
|
48
|
+
ensureSchema(): Promise<void>;
|
|
49
|
+
/** Returns whether this ref's mirror is now current. `false` means another process is rebuilding it. */
|
|
50
|
+
ensureRef(constant: ConstantModel, database: DatabaseModel): Promise<boolean>;
|
|
51
|
+
/** Re-runs the refs another process was holding. Returns how many are still outstanding. */
|
|
52
|
+
retryPending(): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* Rebuilds one ref's mirror rows in id-ordered chunks so a large table does not block the boot. `onChunk` runs
|
|
55
|
+
* between chunks and reports whether this process still holds the claim: a backfill that outlives the lock TTL
|
|
56
|
+
* would otherwise keep writing rows underneath the `DELETE` of the process that took over. Returns whether the
|
|
57
|
+
* whole table was covered.
|
|
58
|
+
*/
|
|
59
|
+
reconcileRef(ref: string, columns: SearchColumns, onChunk?: () => Promise<boolean>): Promise<boolean>;
|
|
60
|
+
/** Merges accumulated fts5 segments. Returns whether this process was the one that did the work. */
|
|
61
|
+
optimize(): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Drops a ref's triggers so a bulk import skips the per-row mirror write. Pair with `resume`.
|
|
64
|
+
*
|
|
65
|
+
* The hash is cleared here rather than in `resume` alone: a process that dies mid-import never reaches `resume`,
|
|
66
|
+
* and a boot that finds the hash intact recreates the triggers and skips the backfill, leaving everything
|
|
67
|
+
* written in between missing from the mirror for good.
|
|
68
|
+
*/
|
|
69
|
+
suspend(database: DatabaseModel): Promise<void>;
|
|
70
|
+
/** Returns whether the mirror is current again; `false` means another process holds the rebuild. */
|
|
71
|
+
resume(constant: ConstantModel, database: DatabaseModel): Promise<boolean>;
|
|
72
|
+
}
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const quoteIdent: (identifier: string) => string;
|
|
2
|
+
export declare const jsonPath: (path: string) => string;
|
|
3
|
+
export declare const stableJson: (value: unknown) => string;
|
|
4
|
+
/** Stable hash of a schema descriptor, stored in `_akan_meta` so a changed definition is detected without migrations. */
|
|
5
|
+
export declare const descriptorHash: (value: unknown) => Promise<string>;
|
|
@@ -32,6 +32,12 @@ export declare class SignalContext<Ctx extends HttpExecutionContext | WebSocketE
|
|
|
32
32
|
getAdaptor<T extends Adaptor>(adaptorCls: AdaptorCls<T>): T;
|
|
33
33
|
getService<T>(refName: string): T;
|
|
34
34
|
init(): Promise<this>;
|
|
35
|
+
/**
|
|
36
|
+
* Re-checks this context's guards outside of a request, for a websocket room that is already
|
|
37
|
+
* subscribed. Only global middlewares run: they carry the account resolution this depends on,
|
|
38
|
+
* while endpoint middlewares (cache/timeout/retry) would observe a call that never executes.
|
|
39
|
+
*/
|
|
40
|
+
authorize(): Promise<boolean>;
|
|
35
41
|
exec(): Promise<Response | undefined>;
|
|
36
42
|
static try(endpoint: Adaptor, endpointInfo: EndpointInfo, key: string, fn: () => Promise<Response | undefined>): Promise<Response | undefined>;
|
|
37
43
|
static resolveReturn(value: unknown, { signalContext, returnRef, arrDepth, registry, live, }: {
|
|
@@ -47,6 +53,7 @@ export declare class SignalContext<Ctx extends HttpExecutionContext | WebSocketE
|
|
|
47
53
|
}): Promise<unknown>;
|
|
48
54
|
getHttpContext<Appended = unknown>(): HttpExecutionContext<Appended>;
|
|
49
55
|
getWebSocketContext<Appended = unknown>(): WebSocketExecutionContext<Appended>;
|
|
56
|
+
get<T = unknown>(key: string): T | null;
|
|
50
57
|
getRoomId(key: string): string;
|
|
51
58
|
getEnv(): Env;
|
|
52
59
|
getArg<T = unknown>(argName: string): T | undefined;
|
package/types/signal/types.d.ts
CHANGED
|
@@ -147,5 +147,9 @@ export type WebsocketPublishData = {
|
|
|
147
147
|
roomId: string;
|
|
148
148
|
data: object | object[];
|
|
149
149
|
};
|
|
150
|
-
export type
|
|
150
|
+
export type WebsocketAuthAck = {
|
|
151
|
+
type: "auth";
|
|
152
|
+
revokedRooms: string[];
|
|
153
|
+
};
|
|
154
|
+
export type WebsocketResData = WebsocketMessageData | WebsocketSubscribeAck | WebsocketPublishData | WebsocketAuthAck;
|
|
151
155
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EnumInstance } from "akanjs/base";
|
|
2
|
-
import { type ConstantCls, type ConstantField } from "akanjs/constant";
|
|
2
|
+
import { type ConstantCls, type ConstantField, type TextFieldRole } from "akanjs/constant";
|
|
3
3
|
export declare const databaseModelVariants: readonly ["input", "object", "full", "light", "insight"];
|
|
4
4
|
export type DatabaseModelVariant = (typeof databaseModelVariants)[number];
|
|
5
5
|
export interface ConstantSchemaOptions {
|
|
@@ -75,7 +75,7 @@ export interface FieldSchema {
|
|
|
75
75
|
defaultLabel?: string;
|
|
76
76
|
exampleLabel?: string;
|
|
77
77
|
constraints: string[];
|
|
78
|
-
text?:
|
|
78
|
+
text?: TextFieldRole;
|
|
79
79
|
meta: Record<string, unknown>;
|
|
80
80
|
raw: ConstantField;
|
|
81
81
|
}
|
package/ui/Constant/schemaDoc.ts
CHANGED
|
@@ -7,7 +7,13 @@ import {
|
|
|
7
7
|
type PrimitiveScalar,
|
|
8
8
|
} from "akanjs/base";
|
|
9
9
|
import { capitalize } from "akanjs/common";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type ConstantCls,
|
|
12
|
+
type ConstantField,
|
|
13
|
+
type ConstantModel,
|
|
14
|
+
ConstantRegistry,
|
|
15
|
+
type TextFieldRole,
|
|
16
|
+
} from "akanjs/constant";
|
|
11
17
|
|
|
12
18
|
export const databaseModelVariants = ["input", "object", "full", "light", "insight"] as const;
|
|
13
19
|
export type DatabaseModelVariant = (typeof databaseModelVariants)[number];
|
|
@@ -93,7 +99,7 @@ export interface FieldSchema {
|
|
|
93
99
|
defaultLabel?: string;
|
|
94
100
|
exampleLabel?: string;
|
|
95
101
|
constraints: string[];
|
|
96
|
-
text?:
|
|
102
|
+
text?: TextFieldRole;
|
|
97
103
|
meta: Record<string, unknown>;
|
|
98
104
|
raw: ConstantField;
|
|
99
105
|
}
|
package/ui/Model/EditModal.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { clsx, router, usePage } from "akanjs/client";
|
|
2
|
+
import { clsx, isRscNavigationFromCache, router, usePage } from "akanjs/client";
|
|
3
3
|
import { capitalize, deepObjectify, lowerlize } from "akanjs/common";
|
|
4
4
|
import { ConstantRegistry, immerify } from "akanjs/constant";
|
|
5
5
|
import type { ClientEdit, ServerEdit, SliceMeta } from "akanjs/fetch";
|
|
@@ -11,6 +11,8 @@ import { AiOutlinePlus, AiOutlineSave } from "react-icons/ai";
|
|
|
11
11
|
import { Button } from "../Button";
|
|
12
12
|
import { Modal } from "../Modal";
|
|
13
13
|
|
|
14
|
+
const EDIT_PAYLOAD_MAX_AGE_MS = 60_000;
|
|
15
|
+
|
|
14
16
|
interface EditModelProps<Full> {
|
|
15
17
|
/** Rendering mode for the edit shell. */
|
|
16
18
|
type?: "modal" | "form" | "empty";
|
|
@@ -148,6 +150,7 @@ export default function EditModal<Full extends { id: string }>({
|
|
|
148
150
|
setModelModal: `set${ModelName}Modal`,
|
|
149
151
|
modelLoading: `${modelName}Loading`,
|
|
150
152
|
modelViewAt: `${modelName}ViewAt`,
|
|
153
|
+
editModel: `edit${ModelName}`,
|
|
151
154
|
newModel: `new${ModelName}`,
|
|
152
155
|
crystalizeModel: `crystalize${ModelName}`,
|
|
153
156
|
modelObj: `${modelName}Obj`,
|
|
@@ -159,10 +162,19 @@ export default function EditModal<Full extends { id: string }>({
|
|
|
159
162
|
(state: unknown) => (state as { [key: string]: { id: string | null } })[names.modelForm].id,
|
|
160
163
|
);
|
|
161
164
|
const modelFormLoading = storeUse[names.modelFormLoading]() as string | boolean;
|
|
165
|
+
const modalId = id ?? ((modelEdit as any)?.[names.modelObj] as Full | undefined)?.id ?? undefined;
|
|
162
166
|
const isModalOpen =
|
|
163
167
|
modelModal === (modal ?? "edit") &&
|
|
164
|
-
(modelFormLoading === false || modelFormLoading ===
|
|
165
|
-
((!modelFormId && !
|
|
168
|
+
(modelFormLoading === false || modelFormLoading === modalId) &&
|
|
169
|
+
((!modelFormId && !modalId) || modalId === modelFormId);
|
|
170
|
+
const isEditPayloadStale = useCallback((viewAt?: Date | null) => {
|
|
171
|
+
if (isRscNavigationFromCache()) return true;
|
|
172
|
+
return (
|
|
173
|
+
viewAt instanceof Date &&
|
|
174
|
+
!Number.isNaN(viewAt.getTime()) &&
|
|
175
|
+
Date.now() - viewAt.getTime() > EDIT_PAYLOAD_MAX_AGE_MS
|
|
176
|
+
);
|
|
177
|
+
}, []);
|
|
166
178
|
useEffect(() => {
|
|
167
179
|
if (!modelEdit) return;
|
|
168
180
|
const refName = (modelEdit as ServerEdit<string, Full>).refName;
|
|
@@ -170,15 +182,21 @@ export default function EditModal<Full extends { id: string }>({
|
|
|
170
182
|
const cnst = ConstantRegistry.getDatabase(modelName);
|
|
171
183
|
const modelRef = cnst.full;
|
|
172
184
|
if (editType === "edit") {
|
|
173
|
-
const
|
|
185
|
+
const modelObj = (modelEdit as any)[names.modelObj] as Full;
|
|
186
|
+
const viewAt = (modelEdit as any)[names.modelViewAt] as Date;
|
|
187
|
+
const crystal = new modelRef().set(modelObj) as unknown as Full;
|
|
174
188
|
st.set({
|
|
175
189
|
[names.model]: crystal,
|
|
176
190
|
[names.modelLoading]: false,
|
|
177
191
|
[names.modelForm]: immerify(modelRef, crystal),
|
|
178
192
|
[names.modelFormLoading]: false,
|
|
179
193
|
[names.modelModal]: modal ?? "edit",
|
|
180
|
-
[names.modelViewAt]:
|
|
194
|
+
[names.modelViewAt]: viewAt,
|
|
181
195
|
});
|
|
196
|
+
if (isEditPayloadStale(viewAt))
|
|
197
|
+
void storeDo[names.editModel](modelObj.id, { modal }).catch(() => {
|
|
198
|
+
st.set({ [names.modelFormLoading]: false });
|
|
199
|
+
});
|
|
182
200
|
} else {
|
|
183
201
|
|
|
184
202
|
const crystal = new modelRef().set(modelEdit as Full) as unknown as Full;
|
|
@@ -186,7 +204,7 @@ export default function EditModal<Full extends { id: string }>({
|
|
|
186
204
|
}
|
|
187
205
|
return () => {
|
|
188
206
|
};
|
|
189
|
-
}, [modelEdit]);
|
|
207
|
+
}, [modelEdit, isEditPayloadStale]);
|
|
190
208
|
|
|
191
209
|
const handleCancel = useCallback(() => {
|
|
192
210
|
const modelForm = (st.get() as any)[names.modelForm] as Full;
|