silosdk 0.0.0 → 0.0.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/README.md +3 -1
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/cli/d1.cjs +93 -0
- package/dist/cli/d1.mjs +92 -0
- package/dist/cli/index.cjs +93 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +94 -0
- package/dist/cli/init.cjs +134 -0
- package/dist/cli/init.mjs +133 -0
- package/dist/cli/kv.cjs +63 -0
- package/dist/cli/kv.mjs +60 -0
- package/dist/cli/r2.cjs +83 -0
- package/dist/cli/r2.mjs +82 -0
- package/dist/cli/wrangler.cjs +93 -0
- package/dist/cli/wrangler.mjs +89 -0
- package/dist/local/adapters/cloudflare.cjs +200 -0
- package/dist/local/adapters/cloudflare.d.cts +50 -0
- package/dist/local/adapters/cloudflare.d.mts +50 -0
- package/dist/local/adapters/cloudflare.mjs +200 -0
- package/dist/local/auth-context.cjs +14 -0
- package/dist/local/auth-context.d.cts +7 -0
- package/dist/local/auth-context.d.mts +7 -0
- package/dist/local/auth-context.mjs +12 -0
- package/dist/local/auth.cjs +109 -0
- package/dist/local/auth.d.cts +26 -0
- package/dist/local/auth.d.mts +26 -0
- package/dist/local/auth.mjs +99 -0
- package/dist/local/commit.cjs +350 -0
- package/dist/local/commit.d.cts +59 -0
- package/dist/local/commit.d.mts +59 -0
- package/dist/local/commit.mjs +349 -0
- package/dist/local/config.cjs +17 -0
- package/dist/local/config.mjs +15 -0
- package/dist/local/index.cjs +16 -0
- package/dist/local/index.d.cts +10 -0
- package/dist/local/index.d.mts +10 -0
- package/dist/local/index.mjs +9 -0
- package/dist/local/provider.cjs +204 -0
- package/dist/local/provider.d.cts +25 -0
- package/dist/local/provider.d.mts +25 -0
- package/dist/local/provider.mjs +203 -0
- package/dist/local/query-store.cjs +276 -0
- package/dist/local/query-store.mjs +274 -0
- package/dist/local/storage.cjs +71 -0
- package/dist/local/storage.d.cts +7 -0
- package/dist/local/storage.d.mts +7 -0
- package/dist/local/storage.mjs +68 -0
- package/dist/local/sync.cjs +124 -0
- package/dist/local/sync.d.cts +36 -0
- package/dist/local/sync.d.mts +36 -0
- package/dist/local/sync.mjs +122 -0
- package/dist/local/view.cjs +257 -0
- package/dist/local/view.d.cts +24 -0
- package/dist/local/view.d.mts +24 -0
- package/dist/local/view.mjs +254 -0
- package/dist/package.cjs +11 -0
- package/dist/package.mjs +5 -0
- package/dist/schema/index.cjs +276 -0
- package/dist/schema/index.d.cts +207 -0
- package/dist/schema/index.d.mts +207 -0
- package/dist/schema/index.mjs +265 -0
- package/dist/server/auth.cjs +132 -0
- package/dist/server/auth.d.cts +49 -0
- package/dist/server/auth.d.mts +49 -0
- package/dist/server/auth.mjs +122 -0
- package/dist/server/d1.cjs +120 -0
- package/dist/server/d1.mjs +116 -0
- package/dist/server/do.cjs +132 -0
- package/dist/server/do.d.cts +21 -0
- package/dist/server/do.d.mts +21 -0
- package/dist/server/do.mjs +131 -0
- package/dist/server/index.cjs +355 -0
- package/dist/server/index.d.cts +65 -0
- package/dist/server/index.d.mts +65 -0
- package/dist/server/index.mjs +348 -0
- package/dist/server/protect.cjs +34 -0
- package/dist/server/protect.d.cts +32 -0
- package/dist/server/protect.d.mts +32 -0
- package/dist/server/protect.mjs +33 -0
- package/dist/server/r2.cjs +58 -0
- package/dist/server/r2.d.cts +4 -0
- package/dist/server/r2.d.mts +4 -0
- package/dist/server/r2.mjs +53 -0
- package/package.json +55 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/local/sync.ts
|
|
2
|
+
/**
|
|
3
|
+
* Abstract sync adapter base class.
|
|
4
|
+
*/
|
|
5
|
+
var SyncAdapter = class {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.merge = options?.merge;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Apply remote ops to local SQLite with LWW conflict resolution.
|
|
12
|
+
*/
|
|
13
|
+
async function applyRemoteOps(db, ops, subscribers, merge) {
|
|
14
|
+
const touchedViews = /* @__PURE__ */ new Set();
|
|
15
|
+
await db.withTransactionAsync(async () => {
|
|
16
|
+
for (const op of ops) if (op.kind === "add") {
|
|
17
|
+
await handleRemoteAdd(db, op, merge);
|
|
18
|
+
touchedViews.add(op.view.name);
|
|
19
|
+
} else if (op.kind === "update") {
|
|
20
|
+
await handleRemoteUpdate(db, op, merge);
|
|
21
|
+
touchedViews.add(op.view.name);
|
|
22
|
+
} else if (op.kind === "remove") {
|
|
23
|
+
await handleRemoteRemove(db, op);
|
|
24
|
+
touchedViews.add(op.view.name);
|
|
25
|
+
} else if (op.kind === "link" || op.kind === "unlink") {
|
|
26
|
+
await handleRemoteLink(db, op);
|
|
27
|
+
touchedViews.add(op.parent.view.name);
|
|
28
|
+
touchedViews.add(op.child.view.name);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
for (const [key, sub] of subscribers) for (const viewName of touchedViews) if (key.includes(viewName)) {
|
|
32
|
+
sub.update();
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function handleRemoteAdd(db, op, merge) {
|
|
37
|
+
const existing = await db.getFirstAsync(`SELECT version, data FROM views WHERE id = ? AND view = ?`, [op.id, op.view.name]);
|
|
38
|
+
if (!existing) await db.runAsync(`INSERT INTO views (id, view, data, createdAt, updatedAt, version)
|
|
39
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
40
|
+
op.id,
|
|
41
|
+
op.view.name,
|
|
42
|
+
JSON.stringify(op.value),
|
|
43
|
+
op.timestamp,
|
|
44
|
+
op.timestamp,
|
|
45
|
+
op.version
|
|
46
|
+
]);
|
|
47
|
+
else if (op.version > existing.version) {
|
|
48
|
+
const finalData = merge ? merge({
|
|
49
|
+
data: JSON.parse(existing.data),
|
|
50
|
+
version: existing.version
|
|
51
|
+
}, {
|
|
52
|
+
data: op.value,
|
|
53
|
+
version: op.version
|
|
54
|
+
}, op.view.name) : op.value;
|
|
55
|
+
await db.runAsync(`UPDATE views SET data = ?, updatedAt = ?, version = ? WHERE id = ? AND view = ?`, [
|
|
56
|
+
JSON.stringify(finalData),
|
|
57
|
+
op.timestamp,
|
|
58
|
+
op.version,
|
|
59
|
+
op.id,
|
|
60
|
+
op.view.name
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function handleRemoteUpdate(db, op, merge) {
|
|
65
|
+
const existing = await db.getFirstAsync(`SELECT version, data FROM views WHERE id = ? AND view = ?`, [op.id, op.view.name]);
|
|
66
|
+
if (!existing) return;
|
|
67
|
+
if (op.version > existing.version) {
|
|
68
|
+
const localData = JSON.parse(existing.data);
|
|
69
|
+
const mergedData = merge ? merge({
|
|
70
|
+
data: localData,
|
|
71
|
+
version: existing.version
|
|
72
|
+
}, {
|
|
73
|
+
data: {
|
|
74
|
+
...localData,
|
|
75
|
+
...op.value
|
|
76
|
+
},
|
|
77
|
+
version: op.version
|
|
78
|
+
}, op.view.name) : {
|
|
79
|
+
...localData,
|
|
80
|
+
...op.value
|
|
81
|
+
};
|
|
82
|
+
await db.runAsync(`UPDATE views SET data = ?, updatedAt = ?, version = ? WHERE id = ? AND view = ?`, [
|
|
83
|
+
JSON.stringify(mergedData),
|
|
84
|
+
op.timestamp,
|
|
85
|
+
op.version,
|
|
86
|
+
op.id,
|
|
87
|
+
op.view.name
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function handleRemoteRemove(db, op) {
|
|
92
|
+
await db.runAsync(`DELETE FROM views WHERE id = ? AND view = ?`, [op.id, op.view.name]);
|
|
93
|
+
await db.runAsync(`DELETE FROM relations WHERE (parent = ? AND pid = ?) OR (child = ? AND cid = ?)`, [
|
|
94
|
+
op.view.name,
|
|
95
|
+
op.id,
|
|
96
|
+
op.view.name,
|
|
97
|
+
op.id
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
async function handleRemoteLink(db, op) {
|
|
101
|
+
if (op.kind === "link") {
|
|
102
|
+
if (!await db.getFirstAsync(`SELECT 1 FROM relations WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`, [
|
|
103
|
+
op.parent.view.name,
|
|
104
|
+
op.parent.id,
|
|
105
|
+
op.child.view.name,
|
|
106
|
+
op.child.id
|
|
107
|
+
])) await db.runAsync(`INSERT INTO relations (parent, pid, child, cid) VALUES (?, ?, ?, ?)`, [
|
|
108
|
+
op.parent.view.name,
|
|
109
|
+
op.parent.id,
|
|
110
|
+
op.child.view.name,
|
|
111
|
+
op.child.id
|
|
112
|
+
]);
|
|
113
|
+
} else await db.runAsync(`DELETE FROM relations WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`, [
|
|
114
|
+
op.parent.view.name,
|
|
115
|
+
op.parent.id,
|
|
116
|
+
op.child.view.name,
|
|
117
|
+
op.child.id
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { SyncAdapter, applyRemoteOps };
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_schema_index = require('../schema/index.cjs');
|
|
3
|
+
const require_auth = require('./auth.cjs');
|
|
4
|
+
const require_query_store = require('./query-store.cjs');
|
|
5
|
+
const require_config = require('./config.cjs');
|
|
6
|
+
let react = require("react");
|
|
7
|
+
let expo_sqlite = require("expo-sqlite");
|
|
8
|
+
let jotai = require("jotai");
|
|
9
|
+
let jotai_family = require("jotai-family");
|
|
10
|
+
let jotai_utils = require("jotai/utils");
|
|
11
|
+
let nanoid_non_secure = require("nanoid/non-secure");
|
|
12
|
+
|
|
13
|
+
//#region src/local/view.ts
|
|
14
|
+
const viewRegistry = /* @__PURE__ */ new Map();
|
|
15
|
+
let _queryStore = null;
|
|
16
|
+
function getQueryStore() {
|
|
17
|
+
if (!_queryStore) _queryStore = new require_query_store.QueryStore();
|
|
18
|
+
return _queryStore;
|
|
19
|
+
}
|
|
20
|
+
const elementsFamilies = /* @__PURE__ */ new Map();
|
|
21
|
+
const listsFamilies = /* @__PURE__ */ new Map();
|
|
22
|
+
function getElementsFamily(name) {
|
|
23
|
+
if (!elementsFamilies.has(name)) {
|
|
24
|
+
const family = (0, jotai_family.atomFamily)((args) => (0, jotai_utils.atomWithRefresh)(async () => {
|
|
25
|
+
const { options, db } = args;
|
|
26
|
+
return await require_schema_index.getRow({
|
|
27
|
+
name,
|
|
28
|
+
options,
|
|
29
|
+
db
|
|
30
|
+
});
|
|
31
|
+
}), (a, b) => require_schema_index.stableStringify(a.options) === require_schema_index.stableStringify(b.options));
|
|
32
|
+
elementsFamilies.set(name, family);
|
|
33
|
+
}
|
|
34
|
+
return elementsFamilies.get(name);
|
|
35
|
+
}
|
|
36
|
+
function getListsFamily(name) {
|
|
37
|
+
if (!listsFamilies.has(name)) {
|
|
38
|
+
const family = (0, jotai_family.atomFamily)((args) => (0, jotai_utils.atomWithRefresh)(async () => {
|
|
39
|
+
const { options, db } = args;
|
|
40
|
+
let count = void 0;
|
|
41
|
+
if (args.offset !== void 0) count = (await db.getFirstAsync(`SELECT COUNT(*) AS count FROM views WHERE view = ?;`, [name]))?.count;
|
|
42
|
+
return {
|
|
43
|
+
count,
|
|
44
|
+
rows: await require_schema_index.getRows({
|
|
45
|
+
name,
|
|
46
|
+
options,
|
|
47
|
+
db,
|
|
48
|
+
offset: args.offset,
|
|
49
|
+
count
|
|
50
|
+
})
|
|
51
|
+
};
|
|
52
|
+
}), (a, b) => require_schema_index.stableStringify(a.options) === require_schema_index.stableStringify(b.options));
|
|
53
|
+
listsFamilies.set(name, family);
|
|
54
|
+
}
|
|
55
|
+
return listsFamilies.get(name);
|
|
56
|
+
}
|
|
57
|
+
function registerView(v) {
|
|
58
|
+
viewRegistry.set(v.name, v);
|
|
59
|
+
}
|
|
60
|
+
function useView(v, options) {
|
|
61
|
+
const scope = options.scope ?? "private";
|
|
62
|
+
(0, react.useMemo)(() => registerView(v), [v]);
|
|
63
|
+
if (scope === "public") return usePublicView(v, options);
|
|
64
|
+
return usePrivateView(v, options);
|
|
65
|
+
}
|
|
66
|
+
function usePrivateView(v, options) {
|
|
67
|
+
const db = (0, expo_sqlite.useSQLiteContext)();
|
|
68
|
+
const { subscribers } = require_config.useConfig();
|
|
69
|
+
const instanceId = (0, react.useRef)((0, nanoid_non_secure.nanoid)()).current;
|
|
70
|
+
const subscriberKey = `${v.name}:${instanceId}`;
|
|
71
|
+
const elementsFamily = getElementsFamily(v.name);
|
|
72
|
+
const [data, refreshData] = (0, jotai.useAtom)((0, react.useMemo)(() => elementsFamily({
|
|
73
|
+
options,
|
|
74
|
+
db
|
|
75
|
+
}), [
|
|
76
|
+
v.name,
|
|
77
|
+
options,
|
|
78
|
+
db
|
|
79
|
+
]));
|
|
80
|
+
(0, react.useEffect)(() => {
|
|
81
|
+
subscribers.set(subscriberKey, { update: () => refreshData() });
|
|
82
|
+
return () => {
|
|
83
|
+
subscribers.delete(subscriberKey);
|
|
84
|
+
};
|
|
85
|
+
}, [
|
|
86
|
+
subscriberKey,
|
|
87
|
+
subscribers,
|
|
88
|
+
refreshData
|
|
89
|
+
]);
|
|
90
|
+
const { data: rowData, id, view: view$1, createdAt, updatedAt } = data ?? {};
|
|
91
|
+
return {
|
|
92
|
+
data: rowData,
|
|
93
|
+
id: id ?? null,
|
|
94
|
+
view: view$1 ?? null,
|
|
95
|
+
createdAt: createdAt ?? null,
|
|
96
|
+
updatedAt: updatedAt ?? null,
|
|
97
|
+
refresh: () => refreshData()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function usePublicView(v, options) {
|
|
101
|
+
const { url, getToken, authReady, authReadyPromise } = require_config.useConfig();
|
|
102
|
+
const qs = getQueryStore();
|
|
103
|
+
const key = require_query_store.createCacheKey(v.name, options);
|
|
104
|
+
const fetcher = () => fetchPublicRow(url, v.name, options, getToken);
|
|
105
|
+
if (!authReady) throw authReadyPromise ?? new Promise(() => {});
|
|
106
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
107
|
+
const { data: rowData, id, view: view$1, createdAt, updatedAt } = (0, react.useSyncExternalStore)((cb) => qs.subscribe(key, cb), () => qs.getSnapshot(key), () => null) ?? {};
|
|
108
|
+
const parsedData = rowData;
|
|
109
|
+
const refresh = () => {
|
|
110
|
+
qs.invalidateKey(key);
|
|
111
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
data: parsedData,
|
|
115
|
+
id: id ?? null,
|
|
116
|
+
view: view$1 ?? null,
|
|
117
|
+
createdAt: createdAt ?? null,
|
|
118
|
+
updatedAt: updatedAt ?? null,
|
|
119
|
+
refresh
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function useListView(v, options = {}) {
|
|
123
|
+
const scope = options.scope ?? "private";
|
|
124
|
+
(0, react.useMemo)(() => registerView(v), [v]);
|
|
125
|
+
if (scope === "public") return usePublicListView(v, options);
|
|
126
|
+
return usePrivateListView(v, options);
|
|
127
|
+
}
|
|
128
|
+
function usePrivateListView(v, options) {
|
|
129
|
+
const db = (0, expo_sqlite.useSQLiteContext)();
|
|
130
|
+
const { subscribers } = require_config.useConfig();
|
|
131
|
+
const instanceId = (0, react.useRef)((0, nanoid_non_secure.nanoid)()).current;
|
|
132
|
+
const subscriberKey = `${v.name}:${instanceId}`;
|
|
133
|
+
const [offset, setOffset] = (0, react.useState)(options.take ? 0 : void 0);
|
|
134
|
+
const listsFamily = getListsFamily(v.name);
|
|
135
|
+
const [data, refreshData] = (0, jotai.useAtom)((0, react.useMemo)(() => listsFamily({
|
|
136
|
+
options,
|
|
137
|
+
db,
|
|
138
|
+
offset
|
|
139
|
+
}), [
|
|
140
|
+
v.name,
|
|
141
|
+
options,
|
|
142
|
+
offset,
|
|
143
|
+
db
|
|
144
|
+
]));
|
|
145
|
+
(0, react.useEffect)(() => {
|
|
146
|
+
subscribers.set(subscriberKey, { update: () => refreshData() });
|
|
147
|
+
return () => {
|
|
148
|
+
subscribers.delete(subscriberKey);
|
|
149
|
+
};
|
|
150
|
+
}, [
|
|
151
|
+
subscriberKey,
|
|
152
|
+
subscribers,
|
|
153
|
+
refreshData
|
|
154
|
+
]);
|
|
155
|
+
const { rows, count } = data;
|
|
156
|
+
function hasNext() {
|
|
157
|
+
if (count === void 0 || options.take === void 0 || offset === void 0) return false;
|
|
158
|
+
return offset + options.take < count;
|
|
159
|
+
}
|
|
160
|
+
function hasPrev() {
|
|
161
|
+
if (offset === void 0 || options.take === void 0) return false;
|
|
162
|
+
return offset - options.take >= 0;
|
|
163
|
+
}
|
|
164
|
+
const prev = () => {
|
|
165
|
+
if (offset !== void 0 && hasPrev() && options.take !== void 0) setOffset(offset - options.take);
|
|
166
|
+
};
|
|
167
|
+
const next = () => {
|
|
168
|
+
if (hasNext() && offset !== void 0 && options.take !== void 0) setOffset(offset + options.take);
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
list: rows,
|
|
172
|
+
hasPrev,
|
|
173
|
+
hasNext,
|
|
174
|
+
prev,
|
|
175
|
+
next,
|
|
176
|
+
refresh: () => refreshData()
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function usePublicListView(v, options) {
|
|
180
|
+
const { url, getToken, authReady, authReadyPromise } = require_config.useConfig();
|
|
181
|
+
const qs = getQueryStore();
|
|
182
|
+
const key = require_query_store.createCacheKey(v.name, options);
|
|
183
|
+
const fetcher = () => fetchPublicList(url, v.name, options, getToken);
|
|
184
|
+
if (!authReady) throw authReadyPromise ?? new Promise(() => {});
|
|
185
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
186
|
+
const rows = (0, react.useSyncExternalStore)((cb) => qs.subscribe(key, cb), () => qs.getSnapshot(key), () => []);
|
|
187
|
+
const refresh = () => {
|
|
188
|
+
qs.invalidateKey(key);
|
|
189
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
list: rows,
|
|
193
|
+
hasPrev: () => false,
|
|
194
|
+
hasNext: () => false,
|
|
195
|
+
prev: () => {},
|
|
196
|
+
next: () => {},
|
|
197
|
+
refresh
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function fetchPublicRow(baseUrl, viewName, options, getToken) {
|
|
201
|
+
const params = new URLSearchParams({ id: options.id });
|
|
202
|
+
const headers = {};
|
|
203
|
+
let token = null;
|
|
204
|
+
if (getToken) {
|
|
205
|
+
token = await getToken();
|
|
206
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
207
|
+
}
|
|
208
|
+
const res = await fetch(`${baseUrl}/public/${viewName}?${params.toString()}`, { headers });
|
|
209
|
+
if (res.status === 401) {
|
|
210
|
+
if (token) await require_auth.handleUnauthorized(baseUrl);
|
|
211
|
+
throw new Error("Unauthorized");
|
|
212
|
+
}
|
|
213
|
+
if (!res.ok) throw new Error(`Public read failed: ${res.status}`);
|
|
214
|
+
return res.json();
|
|
215
|
+
}
|
|
216
|
+
async function fetchPublicList(baseUrl, viewName, options, getToken) {
|
|
217
|
+
const headers = { "Content-Type": "application/json" };
|
|
218
|
+
let token = null;
|
|
219
|
+
if (getToken) {
|
|
220
|
+
token = await getToken();
|
|
221
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
222
|
+
}
|
|
223
|
+
const params = new URLSearchParams();
|
|
224
|
+
if (options.where) params.set("where", JSON.stringify(options.where));
|
|
225
|
+
if (options.order) params.set("order", JSON.stringify(options.order));
|
|
226
|
+
if (options.take !== void 0) params.set("take", String(options.take));
|
|
227
|
+
if ("parent" in options && options.parent) {
|
|
228
|
+
const [parentView, parentId] = options.parent;
|
|
229
|
+
params.set("parentView", parentView.name);
|
|
230
|
+
params.set("parentId", parentId);
|
|
231
|
+
}
|
|
232
|
+
if ("child" in options && options.child) {
|
|
233
|
+
const [childView, childId] = options.child;
|
|
234
|
+
params.set("childView", childView.name);
|
|
235
|
+
params.set("childId", childId);
|
|
236
|
+
}
|
|
237
|
+
const url = params.size ? `${baseUrl}/public/${viewName}?${params.toString()}` : `${baseUrl}/public/${viewName}`;
|
|
238
|
+
const res = await fetch(url, {
|
|
239
|
+
method: "GET",
|
|
240
|
+
headers
|
|
241
|
+
});
|
|
242
|
+
if (res.status === 401) {
|
|
243
|
+
if (token) await require_auth.handleUnauthorized(baseUrl);
|
|
244
|
+
throw new Error("Unauthorized");
|
|
245
|
+
}
|
|
246
|
+
if (res.status === 403) throw new Error("Forbidden");
|
|
247
|
+
if (!res.ok) throw new Error(`Public list failed: ${res.status}`);
|
|
248
|
+
return res.json();
|
|
249
|
+
}
|
|
250
|
+
function getQueryStoreInstance() {
|
|
251
|
+
return getQueryStore();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
exports.getQueryStoreInstance = getQueryStoreInstance;
|
|
256
|
+
exports.useListView = useListView;
|
|
257
|
+
exports.useView = useView;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AnyViewsOptions, Infer, ParsedViewResult, Prettify, View, ViewOptions, view } from "../schema/index.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/local/view.d.ts
|
|
4
|
+
|
|
5
|
+
interface ElementViewResult<T extends View> {
|
|
6
|
+
data: Prettify<Infer<T>> | null;
|
|
7
|
+
id: string | null;
|
|
8
|
+
view: string | null;
|
|
9
|
+
createdAt: string | null;
|
|
10
|
+
updatedAt: string | null;
|
|
11
|
+
refresh: () => void;
|
|
12
|
+
}
|
|
13
|
+
interface ListViewResult<T extends View> {
|
|
14
|
+
list: Prettify<ParsedViewResult<Infer<T>>>[];
|
|
15
|
+
hasPrev: () => boolean;
|
|
16
|
+
hasNext: () => boolean;
|
|
17
|
+
prev: () => void;
|
|
18
|
+
next: () => void;
|
|
19
|
+
refresh: () => void;
|
|
20
|
+
}
|
|
21
|
+
declare function useView<SomeView extends View>(v: SomeView, options: ViewOptions): ElementViewResult<SomeView>;
|
|
22
|
+
declare function useListView<SomeView extends View>(v: SomeView, options?: AnyViewsOptions<SomeView['schema']['type']>): ListViewResult<SomeView>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { useListView, useView };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AnyViewsOptions, Infer, ParsedViewResult, Prettify, View, ViewOptions, view } from "../schema/index.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/local/view.d.ts
|
|
4
|
+
|
|
5
|
+
interface ElementViewResult<T extends View> {
|
|
6
|
+
data: Prettify<Infer<T>> | null;
|
|
7
|
+
id: string | null;
|
|
8
|
+
view: string | null;
|
|
9
|
+
createdAt: string | null;
|
|
10
|
+
updatedAt: string | null;
|
|
11
|
+
refresh: () => void;
|
|
12
|
+
}
|
|
13
|
+
interface ListViewResult<T extends View> {
|
|
14
|
+
list: Prettify<ParsedViewResult<Infer<T>>>[];
|
|
15
|
+
hasPrev: () => boolean;
|
|
16
|
+
hasNext: () => boolean;
|
|
17
|
+
prev: () => void;
|
|
18
|
+
next: () => void;
|
|
19
|
+
refresh: () => void;
|
|
20
|
+
}
|
|
21
|
+
declare function useView<SomeView extends View>(v: SomeView, options: ViewOptions): ElementViewResult<SomeView>;
|
|
22
|
+
declare function useListView<SomeView extends View>(v: SomeView, options?: AnyViewsOptions<SomeView['schema']['type']>): ListViewResult<SomeView>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { useListView, useView };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { getRow, getRows, stableStringify, view } from "../schema/index.mjs";
|
|
2
|
+
import { handleUnauthorized } from "./auth.mjs";
|
|
3
|
+
import { QueryStore, createCacheKey } from "./query-store.mjs";
|
|
4
|
+
import { useConfig } from "./config.mjs";
|
|
5
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { useSQLiteContext } from "expo-sqlite";
|
|
7
|
+
import { useAtom } from "jotai";
|
|
8
|
+
import { atomFamily } from "jotai-family";
|
|
9
|
+
import { atomWithRefresh } from "jotai/utils";
|
|
10
|
+
import { nanoid } from "nanoid/non-secure";
|
|
11
|
+
|
|
12
|
+
//#region src/local/view.ts
|
|
13
|
+
const viewRegistry = /* @__PURE__ */ new Map();
|
|
14
|
+
let _queryStore = null;
|
|
15
|
+
function getQueryStore() {
|
|
16
|
+
if (!_queryStore) _queryStore = new QueryStore();
|
|
17
|
+
return _queryStore;
|
|
18
|
+
}
|
|
19
|
+
const elementsFamilies = /* @__PURE__ */ new Map();
|
|
20
|
+
const listsFamilies = /* @__PURE__ */ new Map();
|
|
21
|
+
function getElementsFamily(name) {
|
|
22
|
+
if (!elementsFamilies.has(name)) {
|
|
23
|
+
const family = atomFamily((args) => atomWithRefresh(async () => {
|
|
24
|
+
const { options, db } = args;
|
|
25
|
+
return await getRow({
|
|
26
|
+
name,
|
|
27
|
+
options,
|
|
28
|
+
db
|
|
29
|
+
});
|
|
30
|
+
}), (a, b) => stableStringify(a.options) === stableStringify(b.options));
|
|
31
|
+
elementsFamilies.set(name, family);
|
|
32
|
+
}
|
|
33
|
+
return elementsFamilies.get(name);
|
|
34
|
+
}
|
|
35
|
+
function getListsFamily(name) {
|
|
36
|
+
if (!listsFamilies.has(name)) {
|
|
37
|
+
const family = atomFamily((args) => atomWithRefresh(async () => {
|
|
38
|
+
const { options, db } = args;
|
|
39
|
+
let count = void 0;
|
|
40
|
+
if (args.offset !== void 0) count = (await db.getFirstAsync(`SELECT COUNT(*) AS count FROM views WHERE view = ?;`, [name]))?.count;
|
|
41
|
+
return {
|
|
42
|
+
count,
|
|
43
|
+
rows: await getRows({
|
|
44
|
+
name,
|
|
45
|
+
options,
|
|
46
|
+
db,
|
|
47
|
+
offset: args.offset,
|
|
48
|
+
count
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
}), (a, b) => stableStringify(a.options) === stableStringify(b.options));
|
|
52
|
+
listsFamilies.set(name, family);
|
|
53
|
+
}
|
|
54
|
+
return listsFamilies.get(name);
|
|
55
|
+
}
|
|
56
|
+
function registerView(v) {
|
|
57
|
+
viewRegistry.set(v.name, v);
|
|
58
|
+
}
|
|
59
|
+
function useView(v, options) {
|
|
60
|
+
const scope = options.scope ?? "private";
|
|
61
|
+
useMemo(() => registerView(v), [v]);
|
|
62
|
+
if (scope === "public") return usePublicView(v, options);
|
|
63
|
+
return usePrivateView(v, options);
|
|
64
|
+
}
|
|
65
|
+
function usePrivateView(v, options) {
|
|
66
|
+
const db = useSQLiteContext();
|
|
67
|
+
const { subscribers } = useConfig();
|
|
68
|
+
const instanceId = useRef(nanoid()).current;
|
|
69
|
+
const subscriberKey = `${v.name}:${instanceId}`;
|
|
70
|
+
const elementsFamily = getElementsFamily(v.name);
|
|
71
|
+
const [data, refreshData] = useAtom(useMemo(() => elementsFamily({
|
|
72
|
+
options,
|
|
73
|
+
db
|
|
74
|
+
}), [
|
|
75
|
+
v.name,
|
|
76
|
+
options,
|
|
77
|
+
db
|
|
78
|
+
]));
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
subscribers.set(subscriberKey, { update: () => refreshData() });
|
|
81
|
+
return () => {
|
|
82
|
+
subscribers.delete(subscriberKey);
|
|
83
|
+
};
|
|
84
|
+
}, [
|
|
85
|
+
subscriberKey,
|
|
86
|
+
subscribers,
|
|
87
|
+
refreshData
|
|
88
|
+
]);
|
|
89
|
+
const { data: rowData, id, view: view$1, createdAt, updatedAt } = data ?? {};
|
|
90
|
+
return {
|
|
91
|
+
data: rowData,
|
|
92
|
+
id: id ?? null,
|
|
93
|
+
view: view$1 ?? null,
|
|
94
|
+
createdAt: createdAt ?? null,
|
|
95
|
+
updatedAt: updatedAt ?? null,
|
|
96
|
+
refresh: () => refreshData()
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function usePublicView(v, options) {
|
|
100
|
+
const { url, getToken, authReady, authReadyPromise } = useConfig();
|
|
101
|
+
const qs = getQueryStore();
|
|
102
|
+
const key = createCacheKey(v.name, options);
|
|
103
|
+
const fetcher = () => fetchPublicRow(url, v.name, options, getToken);
|
|
104
|
+
if (!authReady) throw authReadyPromise ?? new Promise(() => {});
|
|
105
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
106
|
+
const { data: rowData, id, view: view$1, createdAt, updatedAt } = useSyncExternalStore((cb) => qs.subscribe(key, cb), () => qs.getSnapshot(key), () => null) ?? {};
|
|
107
|
+
const parsedData = rowData;
|
|
108
|
+
const refresh = () => {
|
|
109
|
+
qs.invalidateKey(key);
|
|
110
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
data: parsedData,
|
|
114
|
+
id: id ?? null,
|
|
115
|
+
view: view$1 ?? null,
|
|
116
|
+
createdAt: createdAt ?? null,
|
|
117
|
+
updatedAt: updatedAt ?? null,
|
|
118
|
+
refresh
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function useListView(v, options = {}) {
|
|
122
|
+
const scope = options.scope ?? "private";
|
|
123
|
+
useMemo(() => registerView(v), [v]);
|
|
124
|
+
if (scope === "public") return usePublicListView(v, options);
|
|
125
|
+
return usePrivateListView(v, options);
|
|
126
|
+
}
|
|
127
|
+
function usePrivateListView(v, options) {
|
|
128
|
+
const db = useSQLiteContext();
|
|
129
|
+
const { subscribers } = useConfig();
|
|
130
|
+
const instanceId = useRef(nanoid()).current;
|
|
131
|
+
const subscriberKey = `${v.name}:${instanceId}`;
|
|
132
|
+
const [offset, setOffset] = useState(options.take ? 0 : void 0);
|
|
133
|
+
const listsFamily = getListsFamily(v.name);
|
|
134
|
+
const [data, refreshData] = useAtom(useMemo(() => listsFamily({
|
|
135
|
+
options,
|
|
136
|
+
db,
|
|
137
|
+
offset
|
|
138
|
+
}), [
|
|
139
|
+
v.name,
|
|
140
|
+
options,
|
|
141
|
+
offset,
|
|
142
|
+
db
|
|
143
|
+
]));
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
subscribers.set(subscriberKey, { update: () => refreshData() });
|
|
146
|
+
return () => {
|
|
147
|
+
subscribers.delete(subscriberKey);
|
|
148
|
+
};
|
|
149
|
+
}, [
|
|
150
|
+
subscriberKey,
|
|
151
|
+
subscribers,
|
|
152
|
+
refreshData
|
|
153
|
+
]);
|
|
154
|
+
const { rows, count } = data;
|
|
155
|
+
function hasNext() {
|
|
156
|
+
if (count === void 0 || options.take === void 0 || offset === void 0) return false;
|
|
157
|
+
return offset + options.take < count;
|
|
158
|
+
}
|
|
159
|
+
function hasPrev() {
|
|
160
|
+
if (offset === void 0 || options.take === void 0) return false;
|
|
161
|
+
return offset - options.take >= 0;
|
|
162
|
+
}
|
|
163
|
+
const prev = () => {
|
|
164
|
+
if (offset !== void 0 && hasPrev() && options.take !== void 0) setOffset(offset - options.take);
|
|
165
|
+
};
|
|
166
|
+
const next = () => {
|
|
167
|
+
if (hasNext() && offset !== void 0 && options.take !== void 0) setOffset(offset + options.take);
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
list: rows,
|
|
171
|
+
hasPrev,
|
|
172
|
+
hasNext,
|
|
173
|
+
prev,
|
|
174
|
+
next,
|
|
175
|
+
refresh: () => refreshData()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function usePublicListView(v, options) {
|
|
179
|
+
const { url, getToken, authReady, authReadyPromise } = useConfig();
|
|
180
|
+
const qs = getQueryStore();
|
|
181
|
+
const key = createCacheKey(v.name, options);
|
|
182
|
+
const fetcher = () => fetchPublicList(url, v.name, options, getToken);
|
|
183
|
+
if (!authReady) throw authReadyPromise ?? new Promise(() => {});
|
|
184
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
185
|
+
const rows = useSyncExternalStore((cb) => qs.subscribe(key, cb), () => qs.getSnapshot(key), () => []);
|
|
186
|
+
const refresh = () => {
|
|
187
|
+
qs.invalidateKey(key);
|
|
188
|
+
qs.ensureFetching(key, fetcher, getToken);
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
list: rows,
|
|
192
|
+
hasPrev: () => false,
|
|
193
|
+
hasNext: () => false,
|
|
194
|
+
prev: () => {},
|
|
195
|
+
next: () => {},
|
|
196
|
+
refresh
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function fetchPublicRow(baseUrl, viewName, options, getToken) {
|
|
200
|
+
const params = new URLSearchParams({ id: options.id });
|
|
201
|
+
const headers = {};
|
|
202
|
+
let token = null;
|
|
203
|
+
if (getToken) {
|
|
204
|
+
token = await getToken();
|
|
205
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
206
|
+
}
|
|
207
|
+
const res = await fetch(`${baseUrl}/public/${viewName}?${params.toString()}`, { headers });
|
|
208
|
+
if (res.status === 401) {
|
|
209
|
+
if (token) await handleUnauthorized(baseUrl);
|
|
210
|
+
throw new Error("Unauthorized");
|
|
211
|
+
}
|
|
212
|
+
if (!res.ok) throw new Error(`Public read failed: ${res.status}`);
|
|
213
|
+
return res.json();
|
|
214
|
+
}
|
|
215
|
+
async function fetchPublicList(baseUrl, viewName, options, getToken) {
|
|
216
|
+
const headers = { "Content-Type": "application/json" };
|
|
217
|
+
let token = null;
|
|
218
|
+
if (getToken) {
|
|
219
|
+
token = await getToken();
|
|
220
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
221
|
+
}
|
|
222
|
+
const params = new URLSearchParams();
|
|
223
|
+
if (options.where) params.set("where", JSON.stringify(options.where));
|
|
224
|
+
if (options.order) params.set("order", JSON.stringify(options.order));
|
|
225
|
+
if (options.take !== void 0) params.set("take", String(options.take));
|
|
226
|
+
if ("parent" in options && options.parent) {
|
|
227
|
+
const [parentView, parentId] = options.parent;
|
|
228
|
+
params.set("parentView", parentView.name);
|
|
229
|
+
params.set("parentId", parentId);
|
|
230
|
+
}
|
|
231
|
+
if ("child" in options && options.child) {
|
|
232
|
+
const [childView, childId] = options.child;
|
|
233
|
+
params.set("childView", childView.name);
|
|
234
|
+
params.set("childId", childId);
|
|
235
|
+
}
|
|
236
|
+
const url = params.size ? `${baseUrl}/public/${viewName}?${params.toString()}` : `${baseUrl}/public/${viewName}`;
|
|
237
|
+
const res = await fetch(url, {
|
|
238
|
+
method: "GET",
|
|
239
|
+
headers
|
|
240
|
+
});
|
|
241
|
+
if (res.status === 401) {
|
|
242
|
+
if (token) await handleUnauthorized(baseUrl);
|
|
243
|
+
throw new Error("Unauthorized");
|
|
244
|
+
}
|
|
245
|
+
if (res.status === 403) throw new Error("Forbidden");
|
|
246
|
+
if (!res.ok) throw new Error(`Public list failed: ${res.status}`);
|
|
247
|
+
return res.json();
|
|
248
|
+
}
|
|
249
|
+
function getQueryStoreInstance() {
|
|
250
|
+
return getQueryStore();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
export { getQueryStoreInstance, useListView, useView };
|
package/dist/package.cjs
ADDED