akanjs 2.1.0-rc.6 → 2.1.0-rc.8

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.
@@ -0,0 +1,88 @@
1
+ import os from "node:os";
2
+ import type { CapacitorConfig } from "@capacitor/cli";
3
+ import type { AkanMobileTargetConfig, AppScanResult } from "akanjs";
4
+
5
+ const getLocalIP = () => {
6
+ const interfaces = os.networkInterfaces();
7
+ for (const interfaceName in interfaces) {
8
+ const iface = interfaces[interfaceName];
9
+ if (!iface) continue;
10
+ for (const alias of iface) {
11
+ if (alias.family === "IPv4" && !alias.internal) return alias.address;
12
+ }
13
+ }
14
+ return "127.0.0.1";
15
+ };
16
+
17
+ const normalizeBasePath = (basePath: string | undefined) => basePath?.replace(/^\/+|\/+$/g, "");
18
+
19
+ const routeBasePaths = (appInfo: AppScanResult) =>
20
+ new Set(
21
+ appInfo.routes
22
+ .map((route) => route.replace(/^\.\//, "").split("/")[0])
23
+ .filter((segment): segment is string => !!segment && !segment.startsWith("_") && !segment.startsWith("(")),
24
+ );
25
+
26
+ const resolveTarget = (appInfo: AppScanResult, targetName = process.env.AKAN_MOBILE_TARGET) => {
27
+ const targets = appInfo.akanConfig.mobile.targets;
28
+ if (!targets || Object.keys(targets).length === 0) throw new Error("Akan mobile target metadata is missing.");
29
+ if (targetName) {
30
+ const target = targets[targetName];
31
+ if (!target) {
32
+ const basePath = normalizeBasePath(targetName);
33
+ const [template] = Object.values(targets);
34
+ if (basePath && template && routeBasePaths(appInfo).has(basePath))
35
+ return { ...template, name: basePath, basePath };
36
+ throw new Error(`Akan mobile target '${targetName}' was not found.`);
37
+ }
38
+ return target;
39
+ }
40
+ const entries = Object.entries(targets);
41
+ if (entries.length !== 1) throw new Error("AKAN_MOBILE_TARGET is required when multiple mobile targets exist.");
42
+ return entries[0]?.[1] as AkanMobileTargetConfig;
43
+ };
44
+
45
+ const localCsrUrl = (ip: string, target: AkanMobileTargetConfig) => {
46
+ const basePath = normalizeBasePath(target.basePath);
47
+ const port = process.env.AKAN_PUBLIC_CLIENT_PORT ?? process.env.PORT ?? "8282";
48
+ return `http://${ip}:${port}/${basePath ? `${basePath}` : ""}?csr=true`;
49
+ };
50
+
51
+ export const withBase = (
52
+ configImp: (config: CapacitorConfig, target: AkanMobileTargetConfig) => CapacitorConfig = (config) => config,
53
+ appData?: AppScanResult,
54
+ targetName?: string,
55
+ ) => {
56
+ const ip = getLocalIP();
57
+ const appInfo = appData;
58
+ if (!appInfo) throw new Error("withBase requires apps/<app>/akan.app.json metadata.");
59
+ const target = resolveTarget(appInfo, targetName);
60
+ const baseConfig: CapacitorConfig = {
61
+ ...target,
62
+ appId: target.appId,
63
+ appName: target.appName,
64
+ webDir: "dist",
65
+ server:
66
+ process.env.APP_OPERATION_MODE !== "release"
67
+ ? {
68
+ androidScheme: "http",
69
+ url: localCsrUrl(ip, target),
70
+ cleartext: true,
71
+ allowNavigation: [ip, "localhost"],
72
+ }
73
+ : {
74
+ allowNavigation: ["*"],
75
+ },
76
+ plugins: {
77
+ CapacitorCookies: { enabled: true },
78
+ ...target.plugins,
79
+ },
80
+ android: {
81
+ ...target.android,
82
+ },
83
+ ios: {
84
+ ...target.ios,
85
+ },
86
+ };
87
+ return configImp(baseConfig, target);
88
+ };
package/client/device.ts CHANGED
@@ -50,6 +50,11 @@ const getRenderMode = () => globalWithProcess.process?.env?.AKAN_PUBLIC_RENDER_E
50
50
 
51
51
  const getBrowserLanguage = () => globalThis.navigator?.language?.split("-")[0] ?? "en";
52
52
 
53
+ const isNativeTarget = () => {
54
+ if (typeof window === "undefined") return false;
55
+ return Boolean((window as typeof window & { __AKAN_MOBILE_TARGET__?: unknown }).__AKAN_MOBILE_TARGET__);
56
+ };
57
+
53
58
  export const isMobileDevice = () => {
54
59
  if (typeof navigator === "undefined") return false;
55
60
  if (typeof window !== "undefined" && window.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches) return true;
@@ -88,7 +93,7 @@ export class Device {
88
93
  supportLanguages?: string[] | readonly string[];
89
94
  }) {
90
95
  if (Device.instance) return Device.instance;
91
- if (getRenderMode() !== "csr") {
96
+ if (getRenderMode() !== "csr" || !isNativeTarget()) {
92
97
  const device = createWebDevice({ lang, supportLanguages });
93
98
  Device.instance = device;
94
99
  return device;
@@ -93,6 +93,11 @@ export const getFilterSortByKey = (modelRef: FilterCls, key: string) => {
93
93
  return filterMeta.sort[key];
94
94
  };
95
95
 
96
+ export const fillMissingFilterArgs = (filterInfo: FilterInfo, args: unknown[]) => {
97
+ if (args.length >= filterInfo.args.length) return args;
98
+ return [...args, ...Array(filterInfo.args.length - args.length).fill(undefined)];
99
+ };
100
+
96
101
  export type BaseFilterSortKey = "latest" | "oldest";
97
102
  export type BaseFilterQueryKey = "any";
98
103
  export type BaseFilterKey = BaseFilterSortKey | BaseFilterQueryKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.1.0-rc.6",
3
+ "version": "2.1.0-rc.8",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -95,6 +95,11 @@
95
95
  "import": "./client/capacitor.ts",
96
96
  "default": "./client/capacitor.ts"
97
97
  },
98
+ "./capacitor.base.config": {
99
+ "types": "./types/capacitor.base.config.d.ts",
100
+ "import": "./capacitor.base.config.ts",
101
+ "default": "./capacitor.base.config.ts"
102
+ },
98
103
  "./webkit": {
99
104
  "types": "./types/webkit/index.d.ts",
100
105
  "import": "./webkit/index.ts",
@@ -159,6 +164,7 @@
159
164
  "@capacitor/app": "^8.1.0",
160
165
  "@capacitor/browser": "^8.0.3",
161
166
  "@capacitor/camera": "^8.2.0",
167
+ "@capacitor/cli": "^8.3.4",
162
168
  "@capacitor/core": "^8.3.4",
163
169
  "@capacitor/device": "^8.0.2",
164
170
  "@capacitor/geolocation": "^8.2.0",
@@ -211,6 +217,9 @@
211
217
  "@capacitor/camera": {
212
218
  "optional": true
213
219
  },
220
+ "@capacitor/cli": {
221
+ "optional": true
222
+ },
214
223
  "@capacitor/core": {
215
224
  "optional": true
216
225
  },
@@ -10,11 +10,11 @@ import {
10
10
  ConsoleLogger,
11
11
  type DatabaseAdaptor,
12
12
  DatabaseAdaptorRole,
13
+ JsonCompressor,
13
14
  LibsqlDatabase,
14
15
  type LoggingAdaptor,
15
16
  LoggingAdaptorRole,
16
17
  PostgresDatabase,
17
- JsonCompressor,
18
18
  type QueueAdaptor,
19
19
  QueueAdaptorRole,
20
20
  RedisCache,
@@ -11,6 +11,7 @@ import {
11
11
  DocumentSchema,
12
12
  documentQueryHelper,
13
13
  type FindQueryOption,
14
+ fillMissingFilterArgs,
14
15
  getFilterInfoByKey,
15
16
  getFilterMeta,
16
17
  getFilterSortByKey,
@@ -338,7 +339,8 @@ export class DatabaseResolver {
338
339
  const filterInfo = getFilterInfoByKey(database.filter, queryKey);
339
340
  const queryFn = filterInfo.queryFn;
340
341
  if (!queryFn) throw new Error(`No query function for key: ${queryKey}`);
341
- const query = queryFn(...(hasQueryOption ? args.slice(0, -1) : args), documentQueryHelper);
342
+ const queryArgs = fillMissingFilterArgs(filterInfo, hasQueryOption ? args.slice(0, -1) : args);
343
+ const query = queryFn(...queryArgs, documentQueryHelper);
342
344
  const queryOption = hasQueryOption ? lastArg : {};
343
345
  return { query, queryOption };
344
346
  };
@@ -371,18 +373,19 @@ export class DatabaseResolver {
371
373
  return (this as unknown as DatabaseInstance).__pickId(query, queryOption);
372
374
  },
373
375
  [`exists${capitalize(queryKey)}`]: async function (...args: any) {
374
- const query = queryFn(...args, documentQueryHelper);
376
+ const query = queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
375
377
  return (this as unknown as DatabaseInstance).__exists(query);
376
378
  },
377
379
  [`count${capitalize(queryKey)}`]: async function (...args: any) {
378
- const query = queryFn(...args, documentQueryHelper);
380
+ const query = queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
379
381
  return (this as unknown as DatabaseInstance).__count(query);
380
382
  },
381
383
  [`insight${capitalize(queryKey)}`]: async function (...args: any) {
382
- const query = queryFn(...args, documentQueryHelper);
384
+ const query = queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
383
385
  return (this as unknown as DatabaseInstance).__insight(query);
384
386
  },
385
- [`query${capitalize(queryKey)}`]: (...args: any) => queryFn(...args, documentQueryHelper),
387
+ [`query${capitalize(queryKey)}`]: (...args: any) =>
388
+ queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper),
386
389
  });
387
390
  });
388
391
  applyMixins(DatabaseModelInstance, [database.model]);
@@ -8,6 +8,7 @@ import {
8
8
  type Doc,
9
9
  documentQueryHelper,
10
10
  type FindQueryOption,
11
+ fillMissingFilterArgs,
11
12
  getFilterInfoByKey,
12
13
  getFilterMeta,
13
14
  type ListQueryOption,
@@ -116,9 +117,11 @@ export class ServiceResolver {
116
117
  typeof lastArg.skip === "number" ||
117
118
  typeof lastArg.limit === "number" ||
118
119
  typeof lastArg.sort === "string");
119
- const queryFn = getFilterInfoByKey(database.filter, queryKey).queryFn;
120
+ const filterInfo = getFilterInfoByKey(database.filter, queryKey);
121
+ const queryFn = filterInfo.queryFn;
120
122
  if (!queryFn) throw new Error(`No query function for key: ${queryKey}`);
121
- const query = queryFn(...(hasQueryOption ? args.slice(0, -1) : args), documentQueryHelper);
123
+ const queryArgs = fillMissingFilterArgs(filterInfo, hasQueryOption ? args.slice(0, -1) : args);
124
+ const query = queryFn(...queryArgs, documentQueryHelper);
122
125
  const queryOption = hasQueryOption ? lastArg : {};
123
126
  return { query, queryOption };
124
127
  };
@@ -167,7 +170,7 @@ export class ServiceResolver {
167
170
  return this.__insight(query);
168
171
  },
169
172
  [`query${capitalize(queryKey)}`]: function (this: DatabaseService, ...args: any) {
170
- return queryFn(...args, documentQueryHelper);
173
+ return queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
171
174
  },
172
175
  });
173
176
  });
@@ -1003,7 +1003,8 @@ export class SqliteDocumentStore {
1003
1003
  const store = this;
1004
1004
  const original = JSON.parse(JSON.stringify(sanitizeJson(originalData) ?? {})) as Record<string, unknown>;
1005
1005
  const isNew = !originalData.id;
1006
- const doc = Object.assign(Object.create(this.database.doc.prototype), data);
1006
+ const hydratedData = isNew ? this.prepareDocument(data) : data;
1007
+ const doc = Object.assign(Object.create(this.database.doc.prototype), hydratedData);
1007
1008
  Object.defineProperties(doc, {
1008
1009
  set: {
1009
1010
  value(patch: DocumentRecord) {
@@ -0,0 +1,3 @@
1
+ import type { CapacitorConfig } from "@capacitor/cli";
2
+ import type { AkanMobileTargetConfig, AppScanResult } from "akanjs";
3
+ export declare const withBase: (configImp?: (config: CapacitorConfig, target: AkanMobileTargetConfig) => CapacitorConfig, appData?: AppScanResult, targetName?: string) => CapacitorConfig;
@@ -18,6 +18,7 @@ export declare const setFilterMeta: (filterRef: Cls<unknown, {
18
18
  export declare const getFilterInfoByKey: <ArgNames extends string[] = [], Args extends any[] = any[], Model = any>(modelRef: FilterCls, key: string) => FilterInfo<ArgNames, Args, Model>;
19
19
  export declare const setFilterInfoByKey: <ArgNames extends string[] = [], Args extends any[] = any[], Model = any>(modelRef: Cls<Model>, key: string, filterInfo: FilterInfo<ArgNames, Args, Model>) => void;
20
20
  export declare const getFilterSortByKey: (modelRef: FilterCls, key: string) => unknown;
21
+ export declare const fillMissingFilterArgs: (filterInfo: FilterInfo, args: unknown[]) => any[];
21
22
  export type BaseFilterSortKey = "latest" | "oldest";
22
23
  export type BaseFilterQueryKey = "any";
23
24
  export type BaseFilterKey = BaseFilterSortKey | BaseFilterQueryKey;
@@ -1,5 +1,5 @@
1
1
  import type { DatabaseMode } from "akanjs";
2
- import { type AdaptorCls, BlobStorage, type CacheAdaptor, type CompressAdaptor, ConsoleLogger, type DatabaseAdaptor, type LoggingAdaptor, JsonCompressor, type QueueAdaptor, type ScheduleAdaptor, Scheduler, SolidCache, SolidPubSub, SolidQueue, SqliteDatabase, type StorageAdaptor, type WebsocketAdaptor } from "akanjs/service";
2
+ import { type AdaptorCls, BlobStorage, type CacheAdaptor, type CompressAdaptor, ConsoleLogger, type DatabaseAdaptor, JsonCompressor, type LoggingAdaptor, type QueueAdaptor, type ScheduleAdaptor, Scheduler, SolidCache, SolidPubSub, SolidQueue, SqliteDatabase, type StorageAdaptor, type WebsocketAdaptor } from "akanjs/service";
3
3
  export interface PredefinedAdaptor {
4
4
  database: AdaptorCls<DatabaseAdaptor>;
5
5
  cache: AdaptorCls<CacheAdaptor>;