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.
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { type Context, createContext } from "react";
2
3
 
3
4
  /**
@@ -1,4 +1,4 @@
1
- import { pathGet } from "akanjs/common";
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 = (pathGet(key, dictionary, ".", { t: key }) as { t: string }).t;
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 { pathGet } from "akanjs/common";
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 ? pathGet(rest.join("."), model) : model) as { t?: unknown } | null;
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
  }
@@ -1,5 +1,5 @@
1
1
  import type { GetStateObject, ObjectAssign, Prettify } from "akanjs/base";
2
- import { pathGet } from "akanjs/common";
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 = pathGet(msgKey as string, model, ".", { t: key }) as { t: string };
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.58",
3
+ "version": "3.0.0-alpha.59",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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 doc = this.hydrate(this.prepareDocument({ ...data, id, updatedAt: dayjs() }), originalData);
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();
@@ -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;
@@ -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 ? (
@@ -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 (!field.isScalar && field.isClass) example[key] = "ObjectID";
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)