akanjs 3.0.0-alpha.58 → 3.0.0-alpha.59
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/sharedContext.ts +1 -0
- package/client/translator.ts +2 -2
- package/common/index.ts +1 -0
- package/common/pathGetLoose.ts +31 -0
- package/dictionary/DictionaryLookup.ts +2 -2
- package/dictionary/trans.ts +2 -2
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/service/predefinedAdaptor/database.adaptor.ts +17 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/pathGetLoose.d.ts +8 -0
- package/ui/Signal/Object.tsx +1 -1
- package/ui/Signal/makeExample.ts +9 -2
package/client/sharedContext.ts
CHANGED
package/client/translator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pathGetLoose } from "akanjs/common";
|
|
2
2
|
|
|
3
3
|
export interface Dictionary {
|
|
4
4
|
[key: string]: {
|
|
@@ -63,7 +63,7 @@ export class Translator {
|
|
|
63
63
|
static translateByLocale(lang: string, key: string, param?: Record<string, string | number>): string {
|
|
64
64
|
const dictionary = getTranslatorState().langDictionaryMap.get(lang);
|
|
65
65
|
if (!dictionary) return key;
|
|
66
|
-
const msg = (
|
|
66
|
+
const msg = (pathGetLoose(key, dictionary, ".", { t: key }) as { t: string }).t;
|
|
67
67
|
return param ? msg.replace(/{([^}]+)}/g, (_, key: string) => param[key] as string) : msg;
|
|
68
68
|
}
|
|
69
69
|
|
package/common/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
export { mergeVersion } from "./mergeVersion";
|
|
40
40
|
export { objectify } from "./objectify";
|
|
41
41
|
export { pathGet } from "./pathGet";
|
|
42
|
+
export { pathGetLoose } from "./pathGetLoose";
|
|
42
43
|
export { pathSet } from "./pathSet";
|
|
43
44
|
export { randomPick } from "./randomPick";
|
|
44
45
|
export { randomPicks } from "./randomPicks";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type Indexable = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
const isIndexable = (value: unknown): value is Indexable => Object(value) === value;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a dotted path whose segments may themselves contain the separator.
|
|
7
|
+
*
|
|
8
|
+
* Dictionary keys are built as `<refName>.<value>` and an enum value is a real-world identifier — `gpt-5.6-terra`,
|
|
9
|
+
* `v1.2` — so the key is not a clean dotted path and `pathGet` splits it into segments that were never nodes.
|
|
10
|
+
* The tree stores such a key literally, so resolution has to try joined prefixes too.
|
|
11
|
+
*/
|
|
12
|
+
export const pathGetLoose = (
|
|
13
|
+
path: string | readonly string[],
|
|
14
|
+
obj: unknown,
|
|
15
|
+
separator = ".",
|
|
16
|
+
fallback: unknown = null,
|
|
17
|
+
): unknown => {
|
|
18
|
+
const walk = (node: unknown, rest: readonly string[]): unknown => {
|
|
19
|
+
if (!rest.length) return node;
|
|
20
|
+
if (!isIndexable(node)) return undefined;
|
|
21
|
+
|
|
22
|
+
for (let take = 1; take <= rest.length; take += 1) {
|
|
23
|
+
const child = node[rest.slice(0, take).join(separator)];
|
|
24
|
+
if (child === undefined) continue;
|
|
25
|
+
const found = walk(child, rest.slice(take));
|
|
26
|
+
if (found !== undefined) return found;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
return walk(obj, Array.isArray(path) ? [...path] : (path as string).split(separator)) ?? fallback;
|
|
31
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pathGetLoose } from "akanjs/common";
|
|
2
2
|
import { DictionaryRegistry } from "./dictionaryRegistry";
|
|
3
3
|
import type { DictionaryNode } from "./trans";
|
|
4
4
|
|
|
@@ -23,7 +23,7 @@ export class DictionaryLookup {
|
|
|
23
23
|
const [refName, ...rest] = key.split(".");
|
|
24
24
|
if (!refName) return undefined;
|
|
25
25
|
const model = this.#models[refName];
|
|
26
|
-
const node = (rest.length ?
|
|
26
|
+
const node = (rest.length ? pathGetLoose(rest, model) : model) as { t?: unknown } | null;
|
|
27
27
|
const text = node?.t;
|
|
28
28
|
return typeof text === "string" && text.length ? text : undefined;
|
|
29
29
|
}
|
package/dictionary/trans.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GetStateObject, ObjectAssign, Prettify } from "akanjs/base";
|
|
2
|
-
import {
|
|
2
|
+
import { pathGetLoose } from "akanjs/common";
|
|
3
3
|
|
|
4
4
|
import { DictionaryRegistry } from "./dictionaryRegistry";
|
|
5
5
|
import type { DictModule } from "./locale";
|
|
@@ -177,7 +177,7 @@ export const makeTrans = <
|
|
|
177
177
|
const msgKey = msgKeys.join(".");
|
|
178
178
|
const langDict = rootDictionary[lang] ?? {};
|
|
179
179
|
const model = langDict[modelName as string] ?? {};
|
|
180
|
-
const message =
|
|
180
|
+
const message = pathGetLoose(msgKey as string, model, ".", { t: key }) as { t: string };
|
|
181
181
|
return message.t;
|
|
182
182
|
};
|
|
183
183
|
const getDictionary = (lang: Language) => {
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -981,6 +981,7 @@ export class SqlDocumentStore {
|
|
|
981
981
|
#insertStmt: AkanSqlStatement | null = null;
|
|
982
982
|
#readStmtCache = new Map<string, AkanSqlStatement>();
|
|
983
983
|
#docPrototype: object | null = null;
|
|
984
|
+
#immutableKeys: string[] | null = null;
|
|
984
985
|
|
|
985
986
|
constructor(
|
|
986
987
|
private readonly owner: DocumentDatabaseOwner,
|
|
@@ -1483,7 +1484,9 @@ export class SqlDocumentStore {
|
|
|
1483
1484
|
originalData: DocumentRecord,
|
|
1484
1485
|
{ runSaveHooks = true, crudType = "update" }: WriteHookOptions = {},
|
|
1485
1486
|
) {
|
|
1486
|
-
const
|
|
1487
|
+
const prepared = this.prepareDocument({ ...data, id, updatedAt: dayjs() });
|
|
1488
|
+
this.#assertImmutableUnchanged(prepared, originalData);
|
|
1489
|
+
const doc = this.hydrate(prepared, originalData);
|
|
1487
1490
|
if (runSaveHooks) await this.runHooks("save", crudType, doc, "pre");
|
|
1488
1491
|
await this.runHooks(crudType, crudType, doc, "pre");
|
|
1489
1492
|
const row = this.toRow(doc);
|
|
@@ -1498,6 +1501,19 @@ export class SqlDocumentStore {
|
|
|
1498
1501
|
return doc;
|
|
1499
1502
|
}
|
|
1500
1503
|
|
|
1504
|
+
#assertImmutableUnchanged(prepared: DocumentRecord, originalData: DocumentRecord) {
|
|
1505
|
+
this.#immutableKeys ??= Object.entries(this.database.doc[FIELD_META] as unknown as FieldMap)
|
|
1506
|
+
.filter(([, fieldMeta]) => fieldMeta.getProps().immutable)
|
|
1507
|
+
.map(([key]) => key);
|
|
1508
|
+
if (!this.#immutableKeys.length) return;
|
|
1509
|
+
const changed = this.#immutableKeys.filter((key) => jsonStr(prepared[key]) !== jsonStr(originalData[key]));
|
|
1510
|
+
if (!changed.length) return;
|
|
1511
|
+
|
|
1512
|
+
throw new Error(
|
|
1513
|
+
`Cannot modify immutable field${changed.length > 1 ? "s" : ""} on ${this.table} (${String(prepared.id)}): ${changed.join(", ")}`,
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1501
1517
|
private parseProjectedValue(value: unknown) {
|
|
1502
1518
|
if (typeof value !== "string") return value;
|
|
1503
1519
|
const trimmed = value.trim();
|
package/types/common/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { isMcpDescribableArg, type McpExposureEndpoint, type McpExposureOption,
|
|
|
21
21
|
export { mergeVersion } from "./mergeVersion.d.ts";
|
|
22
22
|
export { objectify } from "./objectify.d.ts";
|
|
23
23
|
export { pathGet } from "./pathGet.d.ts";
|
|
24
|
+
export { pathGetLoose } from "./pathGetLoose.d.ts";
|
|
24
25
|
export { pathSet } from "./pathSet.d.ts";
|
|
25
26
|
export { randomPick } from "./randomPick.d.ts";
|
|
26
27
|
export { randomPicks } from "./randomPicks.d.ts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a dotted path whose segments may themselves contain the separator.
|
|
3
|
+
*
|
|
4
|
+
* Dictionary keys are built as `<refName>.<value>` and an enum value is a real-world identifier — `gpt-5.6-terra`,
|
|
5
|
+
* `v1.2` — so the key is not a clean dotted path and `pathGet` splits it into segments that were never nodes.
|
|
6
|
+
* The tree stores such a key literally, so resolution has to try joined prefixes too.
|
|
7
|
+
*/
|
|
8
|
+
export declare const pathGetLoose: (path: string | readonly string[], obj: unknown, separator?: string, fallback?: unknown) => unknown;
|
package/ui/Signal/Object.tsx
CHANGED
|
@@ -90,7 +90,7 @@ const ObjectDetail = ({ className, objRef }: ObjectDetailProps) => {
|
|
|
90
90
|
<ObjectType objRef={modelRef} arrDepth={arrDepth} nullable={nullable} />
|
|
91
91
|
) : (
|
|
92
92
|
<span className={docPill("muted", "font-mono")}>
|
|
93
|
-
{typeLabel(ConstantRegistry.getModelName(modelRef), arrDepth, nullable)}
|
|
93
|
+
{typeLabel(isMap ? "Map" : ConstantRegistry.getModelName(modelRef), arrDepth, nullable)}
|
|
94
94
|
</span>
|
|
95
95
|
)}
|
|
96
96
|
{isMap ? (
|
package/ui/Signal/makeExample.ts
CHANGED
|
@@ -8,10 +8,15 @@ import {
|
|
|
8
8
|
PrimitiveRegistry,
|
|
9
9
|
type PrimitiveScalar,
|
|
10
10
|
} from "akanjs/base";
|
|
11
|
-
import { type ConstantCls, ConstantRegistry } from "akanjs/constant";
|
|
11
|
+
import { type ConstantCls, type ConstantField, ConstantRegistry } from "akanjs/constant";
|
|
12
12
|
|
|
13
13
|
import type { SerializedArg, SerializedEndpoint, SignalType } from "akanjs/signal";
|
|
14
14
|
|
|
15
|
+
const getMapExample = (field: ConstantField, getValueExample: (modelRef: Cls) => unknown) => {
|
|
16
|
+
const [valueRef, valueArrDepth] = getNonArrayModel(field.of as Cls);
|
|
17
|
+
return { key: arraiedModel(getValueExample(valueRef as Cls), valueArrDepth) };
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
const getResponseExample = (ref: Cls | Cls[]) => {
|
|
16
21
|
const [modelRef, arrDepth] = getNonArrayModel(ref);
|
|
17
22
|
const isPrimitive = PrimitiveRegistry.has(modelRef);
|
|
@@ -21,6 +26,7 @@ const getResponseExample = (ref: Cls | Cls[]) => {
|
|
|
21
26
|
Object.entries((modelRef as ConstantCls)[FIELD_META]).forEach(([key, field]) => {
|
|
22
27
|
if (field.example) example[key] = field.example as unknown;
|
|
23
28
|
else if (field.enum) example[key] = arraiedModel<string>(field.enum.values[0] as string, field.arrDepth);
|
|
29
|
+
else if (field.isMap) example[key] = getMapExample(field, getResponseExample);
|
|
24
30
|
else example[key] = getResponseExample(field.modelRef);
|
|
25
31
|
});
|
|
26
32
|
const result = arraiedModel(example, arrDepth);
|
|
@@ -33,7 +39,8 @@ const getRequestExample = (modelRef: Cls) => {
|
|
|
33
39
|
if (isPrimitive) return (modelRef as typeof PrimitiveScalar)[EXAMPLE_VALUE];
|
|
34
40
|
else {
|
|
35
41
|
Object.entries((modelRef as ConstantCls)[FIELD_META]).forEach(([key, field]) => {
|
|
36
|
-
if (
|
|
42
|
+
if (field.isMap) example[key] = getMapExample(field, getRequestExample);
|
|
43
|
+
else if (!field.isScalar && field.isClass) example[key] = "ObjectID";
|
|
37
44
|
else
|
|
38
45
|
example[key] = (
|
|
39
46
|
(field.example ?? field.enum)
|