@wells-nelson/data-storage 1.0.1 → 1.0.3
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 +54 -7
- package/dist/app.svelte.d.ts +9 -1
- package/dist/app.svelte.js +8 -0
- package/dist/cloud.svelte.d.ts +10 -2
- package/dist/cloud.svelte.js +75 -6
- package/dist/containers.d.ts +26 -6
- package/dist/containers.js +87 -4
- package/dist/database.d.ts +3 -3
- package/dist/dataset.d.ts +3 -3
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +101 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/persisted.svelte.d.ts +4 -1
- package/dist/persisted.svelte.js +7 -3
- package/dist/sql.d.ts +6 -4
- package/dist/sql.js +115 -21
- package/dist/store.d.ts +22 -3
- package/dist/store.js +38 -3
- package/dist/supabase.d.ts +34 -4
- package/dist/supabase.js +79 -17
- package/dist/types.d.ts +10 -6
- package/package.json +1 -1
package/dist/supabase.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createClient } from '@supabase/supabase-js';
|
|
2
1
|
/**
|
|
3
2
|
* PostgREST refuses a schema that isn't exposed in the project's Data API settings (PGRST106,
|
|
4
3
|
* "Invalid schema: …"). Returns the fix to show instead, or null for other errors.
|
|
@@ -24,23 +23,83 @@ export function friendlyError(error, noun, schema = 'public') {
|
|
|
24
23
|
return new Error(`That email already has access to this ${noun}.`);
|
|
25
24
|
return new Error(error.message);
|
|
26
25
|
}
|
|
26
|
+
/** supabase-js's own session key for a project URL. */
|
|
27
|
+
export function defaultStorageKey(supabaseUrl) {
|
|
28
|
+
try {
|
|
29
|
+
return `sb-${new URL(supabaseUrl).hostname.split('.')[0]}-auth-token`;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 'sb-auth-token';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Move a session stored under supabase-js's default key to `storageKey` (once, when the new key
|
|
37
|
+
* is empty). The old key is removed: two apps holding the same refresh token would revoke each
|
|
38
|
+
* other's session, so only the first app to start keeps it and the other signs in again.
|
|
39
|
+
*/
|
|
40
|
+
export function adoptSession(legacyKey, storageKey) {
|
|
41
|
+
if (legacyKey === storageKey)
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
if (localStorage.getItem(storageKey) !== null)
|
|
45
|
+
return;
|
|
46
|
+
const value = localStorage.getItem(legacyKey);
|
|
47
|
+
if (value === null)
|
|
48
|
+
return;
|
|
49
|
+
localStorage.setItem(storageKey, value);
|
|
50
|
+
localStorage.removeItem(legacyKey);
|
|
51
|
+
const verifier = localStorage.getItem(`${legacyKey}-code-verifier`);
|
|
52
|
+
if (verifier !== null) {
|
|
53
|
+
localStorage.setItem(`${storageKey}-code-verifier`, verifier);
|
|
54
|
+
localStorage.removeItem(`${legacyKey}-code-verifier`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* storage unavailable: nothing to move */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
27
61
|
/** Supabase client plus the members API, with table and RPC names from `members`. */
|
|
28
62
|
export function createSupabaseApi(env, members, options = {}) {
|
|
29
63
|
let client = null;
|
|
64
|
+
let loading = null;
|
|
30
65
|
const schema = options.schema ?? 'public';
|
|
66
|
+
const legacyKey = defaultStorageKey(env.supabaseUrl);
|
|
67
|
+
const storageKey = options.storageKey ?? legacyKey;
|
|
31
68
|
const { table, containerField, membersRpc, roles, noun } = members;
|
|
32
69
|
const rpcParam = members.rpcParam ?? `p_${containerField}`;
|
|
33
|
-
function
|
|
70
|
+
function loadClient() {
|
|
34
71
|
if (!env.cloudConfigured)
|
|
35
|
-
return null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
return Promise.resolve(null);
|
|
73
|
+
loading ??= (async () => {
|
|
74
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
75
|
+
adoptSession(legacyKey, storageKey);
|
|
76
|
+
client = createClient(env.supabaseUrl, env.supabaseAnonKey, {
|
|
77
|
+
db: { schema },
|
|
78
|
+
auth: { flowType: 'pkce', persistSession: true, autoRefreshToken: true, detectSessionInUrl: true, storageKey }
|
|
79
|
+
});
|
|
80
|
+
return client;
|
|
81
|
+
})();
|
|
82
|
+
// A failed download (offline on first use) can be retried later.
|
|
83
|
+
loading.catch(() => (loading = null));
|
|
84
|
+
return loading;
|
|
85
|
+
}
|
|
86
|
+
function mayHaveSession() {
|
|
87
|
+
if (!env.cloudConfigured)
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
if (localStorage.getItem(storageKey) !== null || localStorage.getItem(legacyKey) !== null)
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* no storage: only a redirect can carry a session */
|
|
95
|
+
}
|
|
96
|
+
if (typeof location === 'undefined')
|
|
97
|
+
return false;
|
|
98
|
+
const params = new URLSearchParams(location.search);
|
|
99
|
+
return params.has('code') || params.has('error_description') || /access_token|error_description/.test(location.hash);
|
|
41
100
|
}
|
|
42
|
-
function requireClient() {
|
|
43
|
-
const sb =
|
|
101
|
+
async function requireClient() {
|
|
102
|
+
const sb = await loadClient();
|
|
44
103
|
if (!sb)
|
|
45
104
|
throw new Error('Cloud sync is not configured.');
|
|
46
105
|
return sb;
|
|
@@ -52,9 +111,12 @@ export function createSupabaseApi(env, members, options = {}) {
|
|
|
52
111
|
return {
|
|
53
112
|
configured: env.cloudConfigured,
|
|
54
113
|
schema,
|
|
55
|
-
|
|
114
|
+
storageKey,
|
|
115
|
+
loadClient,
|
|
116
|
+
getClient: () => client,
|
|
117
|
+
mayHaveSession,
|
|
56
118
|
async fetchMembers(containerId) {
|
|
57
|
-
const { data, error } = await requireClient().rpc(membersRpc, { [rpcParam]: containerId });
|
|
119
|
+
const { data, error } = await (await requireClient()).rpc(membersRpc, { [rpcParam]: containerId });
|
|
58
120
|
fail(error);
|
|
59
121
|
return (data ?? []);
|
|
60
122
|
},
|
|
@@ -62,21 +124,21 @@ export function createSupabaseApi(env, members, options = {}) {
|
|
|
62
124
|
const row = { [containerField]: containerId, email: email.trim().toLowerCase() };
|
|
63
125
|
if (roles)
|
|
64
126
|
row.role = role ?? 'viewer';
|
|
65
|
-
const { error } = await requireClient().from(table).insert(row);
|
|
127
|
+
const { error } = await (await requireClient()).from(table).insert(row);
|
|
66
128
|
fail(error);
|
|
67
129
|
},
|
|
68
130
|
async setMemberRole(memberId, role) {
|
|
69
131
|
if (!roles)
|
|
70
132
|
throw new Error('Roles are not enabled.');
|
|
71
|
-
const { error } = await requireClient().from(table).update({ role }).eq('id', memberId);
|
|
133
|
+
const { error } = await (await requireClient()).from(table).update({ role }).eq('id', memberId);
|
|
72
134
|
fail(error);
|
|
73
135
|
},
|
|
74
136
|
async removeMember(memberId) {
|
|
75
|
-
const { error } = await requireClient().from(table).delete().eq('id', memberId);
|
|
137
|
+
const { error } = await (await requireClient()).from(table).delete().eq('id', memberId);
|
|
76
138
|
fail(error);
|
|
77
139
|
},
|
|
78
140
|
async leave(containerId, email) {
|
|
79
|
-
const { error } = await requireClient()
|
|
141
|
+
const { error } = await (await requireClient())
|
|
80
142
|
.from(table)
|
|
81
143
|
.delete()
|
|
82
144
|
.eq(containerField, containerId)
|
|
@@ -85,7 +147,7 @@ export function createSupabaseApi(env, members, options = {}) {
|
|
|
85
147
|
},
|
|
86
148
|
async fetchMyMemberships(email) {
|
|
87
149
|
const columns = roles ? `${containerField}, role` : containerField;
|
|
88
|
-
const { data, error } = await requireClient().from(table).select(columns).eq('email', email.toLowerCase());
|
|
150
|
+
const { data, error } = await (await requireClient()).from(table).select(columns).eq('email', email.toLowerCase());
|
|
89
151
|
fail(error);
|
|
90
152
|
const rows = (data ?? []);
|
|
91
153
|
return new Map(rows.map((r) => [r[containerField], roles ? r.role : null]));
|
package/dist/types.d.ts
CHANGED
|
@@ -8,11 +8,15 @@ export interface DocBase {
|
|
|
8
8
|
id: string;
|
|
9
9
|
updated_at: number;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Collection name → document type, e.g. `{ projects: ProjectDoc; tasks: TaskDoc }`, as a `type`
|
|
13
|
+
* alias or an `interface`. Generics constrain with `D extends DocMap<D>` (every key of `D` is a
|
|
14
|
+
* document), which an interface satisfies; a plain `DocMap` is any such map.
|
|
15
|
+
*/
|
|
16
|
+
export type DocMap<D = Record<string, DocBase>> = {
|
|
17
|
+
[K in keyof D]: DocBase;
|
|
14
18
|
};
|
|
15
|
-
export type CollectionName<D extends DocMap
|
|
19
|
+
export type CollectionName<D extends DocMap<D>> = keyof D & string;
|
|
16
20
|
/** Where a container lives: only on this device, or replicated to Supabase. */
|
|
17
21
|
export type Location = 'local' | 'cloud';
|
|
18
22
|
export type Role = 'viewer' | 'editor' | 'owner';
|
|
@@ -25,7 +29,7 @@ export interface Nouns {
|
|
|
25
29
|
* The shape of an app's data. A **container** (a project, a list…) owns every other row through
|
|
26
30
|
* `containerField` and is the unit that gets shared, synced and moved between data sets.
|
|
27
31
|
*/
|
|
28
|
-
export interface StoreSchema<D extends DocMap
|
|
32
|
+
export interface StoreSchema<D extends DocMap<D>, C extends CollectionName<D> = CollectionName<D>> {
|
|
29
33
|
/** Every collection, parents before children (the container first). */
|
|
30
34
|
collections: readonly CollectionName<D>[];
|
|
31
35
|
schemas: {
|
|
@@ -39,4 +43,4 @@ export interface StoreSchema<D extends DocMap, C extends CollectionName<D> = Col
|
|
|
39
43
|
indexContainerField?: boolean;
|
|
40
44
|
}
|
|
41
45
|
/** The container a document belongs to (containers are their own container). */
|
|
42
|
-
export declare function containerIdOf<D extends DocMap
|
|
46
|
+
export declare function containerIdOf<D extends DocMap<D>>(schema: Pick<StoreSchema<D>, 'container' | 'containerField'>, collection: CollectionName<D>, doc: object): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wells-nelson/data-storage",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Offline-first storage for Wells Nelson apps: RxDB + TanStack DB data sets, Supabase sync of shared containers, tombstones, device prefs and a SQL migration generator.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|