@superbright/indexeddb-orm 0.1.1 → 0.1.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 +370 -19
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +1154 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +6 -9
- package/.prettierignore +0 -9
- package/.prettierrc.cjs +0 -13
- package/CONSUMING_APP_GUIDE.md +0 -164
- package/MIGRATION_SUMMARY.md +0 -157
- package/docs/app-store-guide.md +0 -160
- package/docs/property-store.md +0 -192
- package/docs/structured-store-migration.md +0 -129
- package/examples/property-store-migration.ts +0 -164
- package/index.html +0 -29
- package/playground/main.ts +0 -38
- package/src/adapters/dexie.ts +0 -28
- package/src/adapters/structured-store.ts +0 -85
- package/src/adapters/zustand-app.ts +0 -221
- package/src/adapters/zustand-unified.ts +0 -342
- package/src/adapters/zustand.ts +0 -142
- package/src/api/app.ts +0 -270
- package/src/api/favorites.ts +0 -64
- package/src/api/properties.ts +0 -293
- package/src/api/users.ts +0 -66
- package/src/db.ts +0 -185
- package/src/debug.ts +0 -25
- package/src/errors.ts +0 -13
- package/src/index.ts +0 -34
- package/src/schema.ts +0 -48
- package/src/storage.ts +0 -71
- package/src/stores/property.ts +0 -133
- package/src/stores/unified.ts +0 -507
- package/src/units/favorites.ts +0 -19
- package/src/validation.ts +0 -32
- package/test-export.js +0 -6
- package/tests/orm.spec.ts +0 -17
- package/tests/setup.ts +0 -2
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -16
- package/vitest.config.ts +0 -9
package/src/api/properties.ts
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { kvGet, kvSet } from "../storage";
|
|
3
|
-
|
|
4
|
-
// Property-related schemas
|
|
5
|
-
export const ViewedUnitSchema = z.object({
|
|
6
|
-
unitId: z.string(),
|
|
7
|
-
viewedDate: z.string(),
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
export const TourContactDataSchema = z.object({
|
|
11
|
-
timezone: z.string(),
|
|
12
|
-
favouriteUnits: z.array(z.string()).optional(),
|
|
13
|
-
preferences: z.record(z.unknown()),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export const PropertyDataSchema = z.object({
|
|
17
|
-
id: z.string(),
|
|
18
|
-
slug: z.string(),
|
|
19
|
-
favoritedUnits: z.array(z.string()),
|
|
20
|
-
tourContactedOn: z.string().nullable(),
|
|
21
|
-
viewedUnits: z.array(ViewedUnitSchema),
|
|
22
|
-
questionnaireResults: z.unknown().nullable().optional(), // Generic for IFilters
|
|
23
|
-
tourContactData: TourContactDataSchema.nullable().optional(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export const PropertyStoreDataSchema = z.object({
|
|
27
|
-
data: z.record(PropertyDataSchema),
|
|
28
|
-
propertySlug: z.string().nullable(),
|
|
29
|
-
propertyId: z.string().nullable(),
|
|
30
|
-
hasPreviouslySearched: z.array(z.string()),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
export type ViewedUnit = z.infer<typeof ViewedUnitSchema>;
|
|
34
|
-
export type TourContactData = z.infer<typeof TourContactDataSchema>;
|
|
35
|
-
export type PropertyData = z.infer<typeof PropertyDataSchema>;
|
|
36
|
-
export type PropertyStoreData = z.infer<typeof PropertyStoreDataSchema>;
|
|
37
|
-
|
|
38
|
-
// Default state
|
|
39
|
-
const defaultPropertyStoreData: PropertyStoreData = {
|
|
40
|
-
data: {},
|
|
41
|
-
propertySlug: null,
|
|
42
|
-
propertyId: null,
|
|
43
|
-
hasPreviouslySearched: [],
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Core property store API
|
|
47
|
-
export class PropertyStore {
|
|
48
|
-
private async getState(): Promise<PropertyStoreData> {
|
|
49
|
-
const state = await kvGet<PropertyStoreData>("property");
|
|
50
|
-
return state ?? defaultPropertyStoreData;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private async setState(updater: (state: PropertyStoreData) => PropertyStoreData): Promise<void> {
|
|
54
|
-
const currentState = await this.getState();
|
|
55
|
-
const newState = updater(currentState);
|
|
56
|
-
await kvSet("property", newState);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Basic state operations
|
|
60
|
-
async setData(value: Record<string, PropertyData>): Promise<void> {
|
|
61
|
-
await this.setState(state => ({ ...state, data: value }));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async setPropertySlug(slug: string): Promise<void> {
|
|
65
|
-
await this.setState(state => ({ ...state, propertySlug: slug }));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async setPropertyId(id: string): Promise<void> {
|
|
69
|
-
await this.setState(state => ({ ...state, propertyId: id }));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async removeData(key: string): Promise<void> {
|
|
73
|
-
await this.setState(state => ({
|
|
74
|
-
...state,
|
|
75
|
-
data: Object.entries(state.data)
|
|
76
|
-
.filter(([k]) => k !== key)
|
|
77
|
-
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}),
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async clearData(): Promise<void> {
|
|
82
|
-
await this.setState(state => ({ ...state, data: {} }));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async setHasPreviouslySearched(slug: string): Promise<void> {
|
|
86
|
-
await this.setState(state => ({
|
|
87
|
-
...state,
|
|
88
|
-
hasPreviouslySearched: Array.from(
|
|
89
|
-
new Set([...state.hasPreviouslySearched, slug])
|
|
90
|
-
),
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Property-specific operations
|
|
95
|
-
async setTourContactedOn(): Promise<void> {
|
|
96
|
-
await this.setState(state => {
|
|
97
|
-
const propertyId = state.propertyId;
|
|
98
|
-
if (!propertyId) return state;
|
|
99
|
-
|
|
100
|
-
const property = state.data[propertyId];
|
|
101
|
-
if (!property) return state;
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
...state,
|
|
105
|
-
data: {
|
|
106
|
-
...state.data,
|
|
107
|
-
[propertyId]: {
|
|
108
|
-
...property,
|
|
109
|
-
tourContactedOn: new Date().toISOString(),
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async getTourContactedOn(): Promise<string | null> {
|
|
117
|
-
const state = await this.getState();
|
|
118
|
-
const propertyId = state.propertyId;
|
|
119
|
-
if (!propertyId) return null;
|
|
120
|
-
|
|
121
|
-
return state.data[propertyId]?.tourContactedOn ?? null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async setQuestionnaireResults(results: unknown): Promise<void> {
|
|
125
|
-
await this.setState(state => {
|
|
126
|
-
const propertyId = state.propertyId;
|
|
127
|
-
if (!propertyId) return state;
|
|
128
|
-
|
|
129
|
-
const property = state.data[propertyId];
|
|
130
|
-
if (!property) return state;
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
...state,
|
|
134
|
-
data: {
|
|
135
|
-
...state.data,
|
|
136
|
-
[propertyId]: {
|
|
137
|
-
...property,
|
|
138
|
-
questionnaireResults: results,
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async setTourContactData(data: TourContactData): Promise<void> {
|
|
146
|
-
await this.setState(state => {
|
|
147
|
-
const propertyId = state.propertyId;
|
|
148
|
-
if (!propertyId) return state;
|
|
149
|
-
|
|
150
|
-
const property = state.data[propertyId];
|
|
151
|
-
if (!property) return state;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
...state,
|
|
155
|
-
data: {
|
|
156
|
-
...state.data,
|
|
157
|
-
[propertyId]: {
|
|
158
|
-
...property,
|
|
159
|
-
tourContactData: data,
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async toggleFavorite(unitId: string): Promise<void> {
|
|
167
|
-
await this.setState(state => {
|
|
168
|
-
const propertyId = state.propertyId;
|
|
169
|
-
if (!propertyId) return state;
|
|
170
|
-
|
|
171
|
-
const property = state.data[propertyId];
|
|
172
|
-
if (!property) return state;
|
|
173
|
-
|
|
174
|
-
const isFavorited = property.favoritedUnits.includes(unitId);
|
|
175
|
-
const updatedFavoritedUnits = isFavorited
|
|
176
|
-
? property.favoritedUnits.filter((id) => id !== unitId)
|
|
177
|
-
: [...property.favoritedUnits, unitId];
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
...state,
|
|
181
|
-
data: {
|
|
182
|
-
...state.data,
|
|
183
|
-
[propertyId]: {
|
|
184
|
-
...property,
|
|
185
|
-
favoritedUnits: updatedFavoritedUnits,
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async markUnitAsViewed(unitId: string, slug: string): Promise<void> {
|
|
193
|
-
const today = new Date();
|
|
194
|
-
const formattedDate = `${String(today.getMonth() + 1).padStart(
|
|
195
|
-
2,
|
|
196
|
-
"0"
|
|
197
|
-
)}/${String(today.getDate()).padStart(2, "0")}`;
|
|
198
|
-
|
|
199
|
-
await this.setState(state => {
|
|
200
|
-
const propertyId = state.propertyId;
|
|
201
|
-
if (!propertyId) return state;
|
|
202
|
-
|
|
203
|
-
const property = state.data[propertyId];
|
|
204
|
-
if (!property) return state;
|
|
205
|
-
|
|
206
|
-
const updatedViewedUnits = [
|
|
207
|
-
// Remove existing entry if it exists
|
|
208
|
-
...property.viewedUnits.filter((u) => u.unitId !== unitId),
|
|
209
|
-
// Add updated one
|
|
210
|
-
{
|
|
211
|
-
unitId,
|
|
212
|
-
viewedDate: formattedDate,
|
|
213
|
-
},
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
...state,
|
|
218
|
-
data: {
|
|
219
|
-
...state.data,
|
|
220
|
-
[propertyId]: {
|
|
221
|
-
...property,
|
|
222
|
-
viewedUnits: updatedViewedUnits,
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Note: This opens a new window - you might want to handle this in the consuming app
|
|
229
|
-
if (typeof window !== 'undefined') {
|
|
230
|
-
window.open(`//${slug}`, "_blank");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Utility methods for getting specific data
|
|
235
|
-
async getUnitState(unitId: string): Promise<{
|
|
236
|
-
isFavorite: boolean;
|
|
237
|
-
viewedDate: string;
|
|
238
|
-
}> {
|
|
239
|
-
const state = await this.getState();
|
|
240
|
-
const property = state.propertyId ? state.data[state.propertyId] : null;
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
isFavorite: property?.favoritedUnits.includes(unitId) ?? false,
|
|
244
|
-
viewedDate:
|
|
245
|
-
property?.viewedUnits.find((u) => u.unitId === unitId)?.viewedDate ?? "",
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async getPropertyData(propertyId?: string): Promise<PropertyData | null> {
|
|
250
|
-
const state = await this.getState();
|
|
251
|
-
const id = propertyId ?? state.propertyId;
|
|
252
|
-
return id ? state.data[id] ?? null : null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async getCurrentProperty(): Promise<PropertyData | null> {
|
|
256
|
-
const state = await this.getState();
|
|
257
|
-
return state.propertyId ? state.data[state.propertyId] ?? null : null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async getFullState(): Promise<PropertyStoreData> {
|
|
261
|
-
return this.getState();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Initialize property if it doesn't exist
|
|
265
|
-
async initializeProperty(propertyId: string, slug: string): Promise<void> {
|
|
266
|
-
await this.setState(state => {
|
|
267
|
-
if (state.data[propertyId]) {
|
|
268
|
-
return { ...state, propertyId, propertySlug: slug };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
...state,
|
|
273
|
-
propertyId,
|
|
274
|
-
propertySlug: slug,
|
|
275
|
-
data: {
|
|
276
|
-
...state.data,
|
|
277
|
-
[propertyId]: {
|
|
278
|
-
id: propertyId,
|
|
279
|
-
slug,
|
|
280
|
-
favoritedUnits: [],
|
|
281
|
-
tourContactedOn: null,
|
|
282
|
-
viewedUnits: [],
|
|
283
|
-
questionnaireResults: null,
|
|
284
|
-
tourContactData: null,
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
};
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Export singleton instance
|
|
293
|
-
export const propertyStore = new PropertyStore();
|
package/src/api/users.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { getDB } from "../db";
|
|
2
|
-
import { UserSchema, type User } from "../schema";
|
|
3
|
-
|
|
4
|
-
const USER_POINTER_KEY = "user";
|
|
5
|
-
|
|
6
|
-
export type IdGenerator = () => string;
|
|
7
|
-
export const defaultIdGenerator: IdGenerator = () =>
|
|
8
|
-
typeof globalThis.crypto?.randomUUID === "function"
|
|
9
|
-
? globalThis.crypto.randomUUID()
|
|
10
|
-
: (() => {
|
|
11
|
-
const b =
|
|
12
|
-
globalThis.crypto?.getRandomValues?.(new Uint8Array(16)) ??
|
|
13
|
-
Uint8Array.from({ length: 16 }, () => (Math.random() * 256) | 0);
|
|
14
|
-
b[6] = (b[6] & 0x0f) | 0x40; // v4
|
|
15
|
-
b[8] = (b[8] & 0x3f) | 0x80; // variant
|
|
16
|
-
const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
17
|
-
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
18
|
-
})();
|
|
19
|
-
|
|
20
|
-
const getPointerUuid = (row: any) => row?.value?.useruuid ?? row?.value?.uuid;
|
|
21
|
-
|
|
22
|
-
export async function ensureUser(
|
|
23
|
-
gen: IdGenerator = defaultIdGenerator,
|
|
24
|
-
): Promise<User> {
|
|
25
|
-
const db = await getDB();
|
|
26
|
-
|
|
27
|
-
// Fast path
|
|
28
|
-
const ptrUuid = getPointerUuid(await db.kv.get(USER_POINTER_KEY));
|
|
29
|
-
if (ptrUuid) {
|
|
30
|
-
const existing = await db.users.get(ptrUuid);
|
|
31
|
-
if (existing) return UserSchema.parse(existing);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Race-safe creation
|
|
35
|
-
try {
|
|
36
|
-
return await db.transaction("rw", db.kv, db.users, async () => {
|
|
37
|
-
const insideUuid = getPointerUuid(await db.kv.get(USER_POINTER_KEY));
|
|
38
|
-
if (insideUuid) {
|
|
39
|
-
const existing = await db.users.get(insideUuid);
|
|
40
|
-
if (existing) return UserSchema.parse(existing);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const uuid = gen();
|
|
44
|
-
await db.kv.add({ key: USER_POINTER_KEY, value: { useruuid: uuid } }); // claim pointer
|
|
45
|
-
const newUser: User = UserSchema.parse({
|
|
46
|
-
uuid,
|
|
47
|
-
createdAt: new Date().toISOString(),
|
|
48
|
-
});
|
|
49
|
-
await db.users.add(newUser);
|
|
50
|
-
return newUser;
|
|
51
|
-
});
|
|
52
|
-
} catch (e: any) {
|
|
53
|
-
if (e?.name === "ConstraintError") {
|
|
54
|
-
// Lost the race → read winner
|
|
55
|
-
const uuid = getPointerUuid(await db.kv.get(USER_POINTER_KEY));
|
|
56
|
-
if (uuid) {
|
|
57
|
-
const winner = await db.users.get(uuid);
|
|
58
|
-
if (winner) return UserSchema.parse(winner);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
throw e;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const getUserUUID = async (gen?: IdGenerator) =>
|
|
66
|
-
(await ensureUser(gen)).uuid;
|
package/src/db.ts
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
// src/db.ts
|
|
2
|
-
import { OrmDexie } from "./adapters/dexie";
|
|
3
|
-
import { OpenDBError } from "./errors";
|
|
4
|
-
import { SCHEMA_VERSION, UserSchema } from "./schema";
|
|
5
|
-
import {
|
|
6
|
-
validate,
|
|
7
|
-
configureValidation,
|
|
8
|
-
type ValidationMode,
|
|
9
|
-
} from "./validation";
|
|
10
|
-
import type { Table } from "dexie";
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
|
|
13
|
-
const MANIFEST_KEY = "manifest";
|
|
14
|
-
const ManifestSchema = z.object({ schemaVersion: z.number().int() });
|
|
15
|
-
type Manifest = z.infer<typeof ManifestSchema>;
|
|
16
|
-
|
|
17
|
-
export type OrmOptions = {
|
|
18
|
-
dbName?: string;
|
|
19
|
-
onReset?: (reason: "incompatible" | "versionchange" | "blocked") => void;
|
|
20
|
-
onError?: (err: unknown) => void;
|
|
21
|
-
validation?: {
|
|
22
|
-
mode?: ValidationMode; // 'strict' | 'warn' | 'silent'
|
|
23
|
-
onIssue?: (ctx: string, details: unknown) => void;
|
|
24
|
-
validateReads?: boolean; // default: true
|
|
25
|
-
dropInvalidOnRead?: boolean; // default: true (when validateReads)
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
let db: OrmDexie | null = null;
|
|
30
|
-
let openPromise: Promise<OrmDexie> | null = null;
|
|
31
|
-
|
|
32
|
-
// Prevent double-installing hooks on the same instance
|
|
33
|
-
const VALIDATION_INSTALLED = Symbol("validationInstalled");
|
|
34
|
-
|
|
35
|
-
function installValidationHooks(
|
|
36
|
-
instance: OrmDexie,
|
|
37
|
-
vopts: OrmOptions["validation"] | undefined,
|
|
38
|
-
) {
|
|
39
|
-
const anyDb = instance as any;
|
|
40
|
-
if (anyDb[VALIDATION_INSTALLED]) return;
|
|
41
|
-
anyDb[VALIDATION_INSTALLED] = true;
|
|
42
|
-
|
|
43
|
-
const writeHook = <T>(table: Table<T, any>, schema: any, name: string) => {
|
|
44
|
-
table.hook("creating", (_pk, obj) => {
|
|
45
|
-
const v = validate(schema, obj, `${name}.creating`);
|
|
46
|
-
if (!v) throw new Error(`Rejected invalid ${name} on create`);
|
|
47
|
-
return v as T; // allow coercion/stripping
|
|
48
|
-
});
|
|
49
|
-
table.hook("updating", (mods, _pk, obj) => {
|
|
50
|
-
const next = { ...(obj as any), ...(mods as any) };
|
|
51
|
-
const v = validate(schema, next, `${name}.updating`);
|
|
52
|
-
if (!v) return {}; // cancel update in warn/silent
|
|
53
|
-
return mods;
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
writeHook(instance.users, UserSchema, "users");
|
|
58
|
-
|
|
59
|
-
const shouldValidateReads = vopts?.validateReads ?? true;
|
|
60
|
-
if (shouldValidateReads) {
|
|
61
|
-
const dropInvalid = vopts?.dropInvalidOnRead ?? true;
|
|
62
|
-
const readHook = <T>(table: Table<T, any>, schema: any, name: string) => {
|
|
63
|
-
table.hook("reading", (obj) => {
|
|
64
|
-
const v = validate(schema, obj, `${name}.reading`);
|
|
65
|
-
if (v) return v;
|
|
66
|
-
return dropInvalid ? undefined : obj; // pass-through if you prefer
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
readHook(instance.users, UserSchema, "users");
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isProbablyDev(): boolean {
|
|
74
|
-
try {
|
|
75
|
-
// @ts-ignore
|
|
76
|
-
if (typeof import.meta !== "undefined" && import.meta.env)
|
|
77
|
-
// @ts-ignore
|
|
78
|
-
return !!import.meta.env.DEV;
|
|
79
|
-
} catch {}
|
|
80
|
-
try {
|
|
81
|
-
if (typeof process !== "undefined" && process.env)
|
|
82
|
-
return process.env.NODE_ENV !== "production";
|
|
83
|
-
} catch {}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function getDB(opts: OrmOptions = {}): Promise<OrmDexie> {
|
|
88
|
-
if (db) return db;
|
|
89
|
-
if (openPromise) return openPromise;
|
|
90
|
-
|
|
91
|
-
const name = opts.dbName ?? "inresi-orm";
|
|
92
|
-
|
|
93
|
-
openPromise = (async () => {
|
|
94
|
-
try {
|
|
95
|
-
// Configure validation once per open
|
|
96
|
-
const defaultMode: ValidationMode = isProbablyDev() ? "warn" : "strict";
|
|
97
|
-
configureValidation({
|
|
98
|
-
mode: opts.validation?.mode ?? defaultMode,
|
|
99
|
-
onIssue: opts.validation?.onIssue,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const instance = new OrmDexie(name);
|
|
103
|
-
|
|
104
|
-
// Multi-tab friendliness
|
|
105
|
-
instance.on("versionchange", () => {
|
|
106
|
-
try {
|
|
107
|
-
instance.close();
|
|
108
|
-
} finally {
|
|
109
|
-
opts.onReset?.("versionchange");
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
instance.on("blocked", () => {
|
|
113
|
-
opts.onReset?.("blocked");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
await instance.open();
|
|
117
|
-
|
|
118
|
-
// Validate/initialize manifest
|
|
119
|
-
const row = await instance.kv.get(MANIFEST_KEY);
|
|
120
|
-
const saved = (row?.value ?? null) as unknown;
|
|
121
|
-
|
|
122
|
-
if (!saved) {
|
|
123
|
-
await instance.transaction("rw", instance.kv, async () => {
|
|
124
|
-
await instance.kv.put({
|
|
125
|
-
key: MANIFEST_KEY,
|
|
126
|
-
value: { schemaVersion: SCHEMA_VERSION },
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
installValidationHooks(instance, opts.validation);
|
|
130
|
-
db = instance;
|
|
131
|
-
return instance;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const parsed = ManifestSchema.safeParse(saved as Manifest);
|
|
135
|
-
if (!parsed.success || parsed.data.schemaVersion !== SCHEMA_VERSION) {
|
|
136
|
-
await instance.delete();
|
|
137
|
-
const fresh = new OrmDexie(name);
|
|
138
|
-
fresh.on("versionchange", () => {
|
|
139
|
-
try {
|
|
140
|
-
fresh.close();
|
|
141
|
-
} finally {
|
|
142
|
-
opts.onReset?.("versionchange");
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
fresh.on("blocked", () => {
|
|
146
|
-
opts.onReset?.("blocked");
|
|
147
|
-
});
|
|
148
|
-
await fresh.open();
|
|
149
|
-
await fresh.kv.put({
|
|
150
|
-
key: MANIFEST_KEY,
|
|
151
|
-
value: { schemaVersion: SCHEMA_VERSION },
|
|
152
|
-
});
|
|
153
|
-
installValidationHooks(fresh, opts.validation);
|
|
154
|
-
opts.onReset?.("incompatible");
|
|
155
|
-
db = fresh;
|
|
156
|
-
return fresh;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
installValidationHooks(instance, opts.validation);
|
|
160
|
-
db = instance;
|
|
161
|
-
return instance;
|
|
162
|
-
} catch (e) {
|
|
163
|
-
opts.onError?.(e);
|
|
164
|
-
throw new OpenDBError("Failed to open IndexedDB", e);
|
|
165
|
-
} finally {
|
|
166
|
-
openPromise = null; // allow future reuse/retry
|
|
167
|
-
}
|
|
168
|
-
})();
|
|
169
|
-
|
|
170
|
-
return openPromise;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export async function resetDB(dbName?: string) {
|
|
174
|
-
const name = dbName ?? "inresi-orm";
|
|
175
|
-
if (db) {
|
|
176
|
-
try {
|
|
177
|
-
await db.close();
|
|
178
|
-
} catch {
|
|
179
|
-
/* ignore */
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const instance = new OrmDexie(name);
|
|
183
|
-
await instance.delete();
|
|
184
|
-
db = null;
|
|
185
|
-
}
|
package/src/debug.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { getDB } from "./db";
|
|
2
|
-
|
|
3
|
-
export async function debugDump(): Promise<Record<string, unknown[]>> {
|
|
4
|
-
const db = await getDB();
|
|
5
|
-
const out: Record<string, unknown[]> = {};
|
|
6
|
-
for (const t of db.tables) {
|
|
7
|
-
out[t.name] = await t.toArray();
|
|
8
|
-
}
|
|
9
|
-
return out;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function exportJSON(filename = "inresi-orm-export.json"): Promise<void> {
|
|
13
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
14
|
-
throw new Error("exportJSON can only run in a browser.");
|
|
15
|
-
}
|
|
16
|
-
const snapshot = await debugDump();
|
|
17
|
-
const blob = new Blob([JSON.stringify(snapshot, null, 2)], { type: "application/json" });
|
|
18
|
-
const a = document.createElement("a");
|
|
19
|
-
a.href = URL.createObjectURL(blob);
|
|
20
|
-
a.download = filename;
|
|
21
|
-
document.body.appendChild(a);
|
|
22
|
-
a.click();
|
|
23
|
-
a.remove();
|
|
24
|
-
URL.revokeObjectURL(a.href);
|
|
25
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export class SchemaMismatchError extends Error {
|
|
2
|
-
constructor(message: string, public detail?: unknown) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = "SchemaMismatchError";
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class OpenDBError extends Error {
|
|
9
|
-
constructor(message: string, public detail?: unknown) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "OpenDBError";
|
|
12
|
-
}
|
|
13
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export * from "./errors";
|
|
2
|
-
export * from "./schema";
|
|
3
|
-
export * from "./db";
|
|
4
|
-
export * from "./api/users";
|
|
5
|
-
export * from "./debug";
|
|
6
|
-
export * from "./storage";
|
|
7
|
-
export * from "./api/favorites";
|
|
8
|
-
export {
|
|
9
|
-
PropertyStore,
|
|
10
|
-
propertyStore,
|
|
11
|
-
type PropertyData,
|
|
12
|
-
type PropertyStoreData,
|
|
13
|
-
} from "./api/properties";
|
|
14
|
-
|
|
15
|
-
// Export unified store classes and main functionality
|
|
16
|
-
export { UnifiedStore, store } from "./stores/unified";
|
|
17
|
-
export {
|
|
18
|
-
createZustandUnifiedStore,
|
|
19
|
-
createZustandUnifiedStore as createZustandPropertyStore, // Alias for easier migration
|
|
20
|
-
createUseUnitState,
|
|
21
|
-
type ZustandUnifiedStoreState
|
|
22
|
-
} from "./adapters/zustand-unified";
|
|
23
|
-
|
|
24
|
-
// Export structured store with nested actions
|
|
25
|
-
export {
|
|
26
|
-
createStructuredStore,
|
|
27
|
-
createStructuredStoreActions,
|
|
28
|
-
type StructuredStore,
|
|
29
|
-
type StructuredStoreActions
|
|
30
|
-
} from "./adapters/structured-store";
|
|
31
|
-
|
|
32
|
-
export { favorites } from "./units/favorites";
|
|
33
|
-
|
|
34
|
-
export { configureValidation, type ValidationMode } from "./validation";
|
package/src/schema.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const SCHEMA_VERSION = 1 as const;
|
|
4
|
-
|
|
5
|
-
// Entities
|
|
6
|
-
export const UserSchema = z.object({
|
|
7
|
-
uuid: z.string().uuid(),
|
|
8
|
-
});
|
|
9
|
-
export type User = z.infer<typeof UserSchema>;
|
|
10
|
-
|
|
11
|
-
// favorites
|
|
12
|
-
export const FavoritesSchema = z.object({
|
|
13
|
-
propertyId: z.string() || z.number(),
|
|
14
|
-
unitIds: z.array(z.string()),
|
|
15
|
-
});
|
|
16
|
-
export type Favorites = z.infer<typeof FavoritesSchema>;
|
|
17
|
-
|
|
18
|
-
// Re-export property schemas from the API module
|
|
19
|
-
export {
|
|
20
|
-
ViewedUnitSchema,
|
|
21
|
-
TourContactDataSchema,
|
|
22
|
-
PropertyDataSchema,
|
|
23
|
-
PropertyStoreDataSchema,
|
|
24
|
-
type ViewedUnit,
|
|
25
|
-
type TourContactData,
|
|
26
|
-
type PropertyData,
|
|
27
|
-
type PropertyStoreData,
|
|
28
|
-
} from "./api/properties";
|
|
29
|
-
|
|
30
|
-
// Re-export app schemas from the API module
|
|
31
|
-
export {
|
|
32
|
-
UnitDataSchema,
|
|
33
|
-
FiltersSchema,
|
|
34
|
-
QueryParamsSchema,
|
|
35
|
-
AppStoreDataSchema,
|
|
36
|
-
type UnitData,
|
|
37
|
-
type Filters,
|
|
38
|
-
type QueryParams,
|
|
39
|
-
type AppStoreData,
|
|
40
|
-
type ResultsMode,
|
|
41
|
-
type SortBy,
|
|
42
|
-
} from "./api/app";
|
|
43
|
-
|
|
44
|
-
// Re-export unified store schemas
|
|
45
|
-
export {
|
|
46
|
-
UnifiedStoreDataSchema,
|
|
47
|
-
type UnifiedStoreData,
|
|
48
|
-
} from "./stores/unified";
|