@superbright/indexeddb-orm 0.1.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.
@@ -0,0 +1,142 @@
1
+ import { propertyStore, type PropertyData, type TourContactData } from "../api/properties";
2
+
3
+ // Zustand-compatible store creation helper
4
+ export interface ZustandPropertyStoreState {
5
+ data: Record<string, PropertyData>;
6
+ propertySlug: string | null;
7
+ propertyId: string | null;
8
+ hasPreviouslySearched: string[];
9
+
10
+ // Async actions
11
+ setData: (value: Record<string, PropertyData>) => Promise<void>;
12
+ setPropertySlug: (slug: string) => Promise<void>;
13
+ setPropertyId: (id: string) => Promise<void>;
14
+ removeData: (key: string) => Promise<void>;
15
+ clearData: () => Promise<void>;
16
+ setHasPreviouslySearched: (slug: string) => Promise<void>;
17
+ toggleFavorite: (unitId: string) => Promise<void>;
18
+ markUnitAsViewed: (unitId: string, slug: string) => Promise<void>;
19
+ setTourContactedOn: () => Promise<void>;
20
+ getTourContactedOn: () => Promise<string | null>;
21
+ setQuestionnaireResults: (results: unknown) => Promise<void>;
22
+ setTourContactData: (data: TourContactData) => Promise<void>;
23
+ initializeProperty: (propertyId: string, slug: string) => Promise<void>;
24
+
25
+ // Sync getters
26
+ getUnitState: (unitId: string) => {
27
+ isFavorite: boolean;
28
+ viewedDate: string;
29
+ };
30
+
31
+ // Internal hydration
32
+ _hydrate: () => Promise<void>;
33
+ }
34
+
35
+ export function createZustandPropertyStore() {
36
+ return (set: any, get: any): ZustandPropertyStoreState => {
37
+ // Helper to update local state after ORM operations
38
+ const syncState = async () => {
39
+ const ormState = await propertyStore.getFullState();
40
+ set({
41
+ data: ormState.data,
42
+ propertySlug: ormState.propertySlug,
43
+ propertyId: ormState.propertyId,
44
+ hasPreviouslySearched: ormState.hasPreviouslySearched,
45
+ });
46
+ };
47
+
48
+ return {
49
+ // Initial state
50
+ data: {},
51
+ propertySlug: null,
52
+ propertyId: null,
53
+ hasPreviouslySearched: [],
54
+
55
+ // Actions that modify state
56
+ async setData(value) {
57
+ await propertyStore.setData(value);
58
+ set({ data: value });
59
+ },
60
+
61
+ async setPropertySlug(slug) {
62
+ await propertyStore.setPropertySlug(slug);
63
+ set({ propertySlug: slug });
64
+ },
65
+
66
+ async setPropertyId(id) {
67
+ await propertyStore.setPropertyId(id);
68
+ set({ propertyId: id });
69
+ },
70
+
71
+ async removeData(key) {
72
+ await propertyStore.removeData(key);
73
+ await syncState();
74
+ },
75
+
76
+ async clearData() {
77
+ await propertyStore.clearData();
78
+ set({ data: {} });
79
+ },
80
+
81
+ async setHasPreviouslySearched(slug) {
82
+ await propertyStore.setHasPreviouslySearched(slug);
83
+ await syncState();
84
+ },
85
+
86
+ async toggleFavorite(unitId) {
87
+ await propertyStore.toggleFavorite(unitId);
88
+ await syncState();
89
+ },
90
+
91
+ async markUnitAsViewed(unitId, slug) {
92
+ await propertyStore.markUnitAsViewed(unitId, slug);
93
+ await syncState();
94
+ },
95
+
96
+ async setTourContactedOn() {
97
+ await propertyStore.setTourContactedOn();
98
+ await syncState();
99
+ },
100
+
101
+ getTourContactedOn: propertyStore.getTourContactedOn.bind(propertyStore),
102
+
103
+ async setQuestionnaireResults(results) {
104
+ await propertyStore.setQuestionnaireResults(results);
105
+ await syncState();
106
+ },
107
+
108
+ async setTourContactData(data) {
109
+ await propertyStore.setTourContactData(data);
110
+ await syncState();
111
+ },
112
+
113
+ async initializeProperty(propertyId, slug) {
114
+ await propertyStore.initializeProperty(propertyId, slug);
115
+ await syncState();
116
+ },
117
+
118
+ // Sync getter for unit state
119
+ getUnitState(unitId: string) {
120
+ const state = get();
121
+ const property = state.propertyId ? state.data[state.propertyId] : null;
122
+
123
+ return {
124
+ isFavorite: property?.favoritedUnits.includes(unitId) ?? false,
125
+ viewedDate:
126
+ property?.viewedUnits.find((u: any) => u.unitId === unitId)?.viewedDate ?? "",
127
+ };
128
+ },
129
+
130
+ // Internal hydration method
131
+ async _hydrate() {
132
+ await syncState();
133
+ },
134
+ };
135
+ };
136
+ }
137
+
138
+ // Helper hook factory for React apps
139
+ export function createUseUnitState() {
140
+ return (useStore: any) => (unitId: string) =>
141
+ useStore((state: ZustandPropertyStoreState) => state.getUnitState(unitId));
142
+ }
package/src/api/app.ts ADDED
@@ -0,0 +1,270 @@
1
+ import { z } from "zod";
2
+ import { kvGet, kvSet } from "../storage";
3
+
4
+ // App-related schemas
5
+ export const UnitDataSchema = z.object({
6
+ isFavorite: z.boolean().optional(),
7
+ viewedDate: z.string().optional(),
8
+ });
9
+
10
+ export const FiltersSchema = z.object({
11
+ availability: z.union([z.string(), z.array(z.string())]).nullable().optional(),
12
+ bedrooms: z.array(z.number()).nullable().optional(),
13
+ cost: z.number().nullable().optional(),
14
+ highlights: z.array(z.string()).optional(),
15
+ });
16
+
17
+ export const QueryParamsSchema = z.object({
18
+ limit: z.number().default(10),
19
+ page: z.number().default(1),
20
+ availability: z.array(z.string()).optional(),
21
+ bedrooms: z.array(z.number()).optional(),
22
+ cost: z.number().nullable().optional(),
23
+ highlights: z.array(z.string()).optional(),
24
+ });
25
+
26
+ export const AppStoreDataSchema = z.object({
27
+ data: z.record(UnitDataSchema),
28
+ filters: FiltersSchema,
29
+ tempFilters: FiltersSchema,
30
+ apiFilters: QueryParamsSchema,
31
+ resultsMode: z.enum(["all", "bestFit", "closestMatch", "favorites"]),
32
+ propertySlug: z.string().nullable(),
33
+ resolvedQuestionnaireValues: z.record(z.array(z.string())),
34
+ sortBy: z.enum(["relevance", "newest", "priceLowToHigh", "priceHighToLow"]),
35
+ filtersLoaded: z.boolean(),
36
+ });
37
+
38
+ export type UnitData = z.infer<typeof UnitDataSchema>;
39
+ export type Filters = z.infer<typeof FiltersSchema>;
40
+ export type QueryParams = z.infer<typeof QueryParamsSchema>;
41
+ export type AppStoreData = z.infer<typeof AppStoreDataSchema>;
42
+ export type ResultsMode = "all" | "bestFit" | "closestMatch" | "favorites";
43
+ export type SortBy = "relevance" | "newest" | "priceLowToHigh" | "priceHighToLow";
44
+
45
+ // Default values
46
+ const defaultFilters: Filters = {
47
+ availability: undefined,
48
+ bedrooms: undefined,
49
+ cost: undefined,
50
+ highlights: undefined,
51
+ };
52
+
53
+ const defaultAppStoreData: AppStoreData = {
54
+ data: {},
55
+ filters: defaultFilters,
56
+ tempFilters: defaultFilters,
57
+ apiFilters: {
58
+ limit: 10,
59
+ page: 1,
60
+ },
61
+ resultsMode: "all",
62
+ propertySlug: null,
63
+ resolvedQuestionnaireValues: {},
64
+ sortBy: "relevance",
65
+ filtersLoaded: false,
66
+ };
67
+
68
+ // Core app store API
69
+ export class AppStore {
70
+ private async getState(): Promise<AppStoreData> {
71
+ const state = await kvGet<AppStoreData>("app");
72
+ return state ?? defaultAppStoreData;
73
+ }
74
+
75
+ private async setState(updater: (state: AppStoreData) => AppStoreData): Promise<void> {
76
+ const currentState = await this.getState();
77
+ const newState = updater(currentState);
78
+ await kvSet("app", newState);
79
+ }
80
+
81
+ // Unit data operations
82
+ async setData(key: string, value: UnitData): Promise<void> {
83
+ await this.setState(state => ({
84
+ ...state,
85
+ data: { ...state.data, [key]: value }
86
+ }));
87
+ }
88
+
89
+ async removeData(key: string): Promise<void> {
90
+ await this.setState(state => {
91
+ const { [key]: removed, ...rest } = state.data;
92
+ return { ...state, data: rest };
93
+ });
94
+ }
95
+
96
+ async clearData(): Promise<void> {
97
+ await this.setState(state => ({ ...state, data: {} }));
98
+ }
99
+
100
+ // Filter operations
101
+ async setFilters(filters: Partial<Filters>): Promise<void> {
102
+ await this.setState(state => ({
103
+ ...state,
104
+ filters: { ...state.filters, ...filters }
105
+ }));
106
+ }
107
+
108
+ async setTempFilters(filters: Partial<Filters>): Promise<void> {
109
+ await this.setState(state => ({
110
+ ...state,
111
+ tempFilters: { ...state.tempFilters, ...filters }
112
+ }));
113
+ }
114
+
115
+ async setFiltersToDefault(): Promise<void> {
116
+ await this.setState(state => ({ ...state, filters: defaultFilters }));
117
+ }
118
+
119
+ async setApiFilters(filters: Partial<QueryParams>): Promise<void> {
120
+ await this.setState(state => ({
121
+ ...state,
122
+ apiFilters: { ...state.apiFilters, ...filters }
123
+ }));
124
+ }
125
+
126
+ // Results and sorting
127
+ async setResultsMode(mode: ResultsMode): Promise<void> {
128
+ await this.setState(state => ({ ...state, resultsMode: mode }));
129
+ }
130
+
131
+ async setSortBy(sortBy: SortBy): Promise<void> {
132
+ await this.setState(state => ({ ...state, sortBy }));
133
+ }
134
+
135
+ // Property operations
136
+ async setPropertySlug(slug: string): Promise<void> {
137
+ await this.setState(state => ({ ...state, propertySlug: slug }));
138
+ }
139
+
140
+ async getResultsUrl(): Promise<string | null> {
141
+ const state = await this.getState();
142
+ return state.propertySlug ? `/${state.propertySlug}/results` : null;
143
+ }
144
+
145
+ // Questionnaire values
146
+ async setResolvedQuestionnaireValues(name: string, values: string[]): Promise<void> {
147
+ await this.setState(state => ({
148
+ ...state,
149
+ resolvedQuestionnaireValues: {
150
+ ...state.resolvedQuestionnaireValues,
151
+ [name]: values
152
+ }
153
+ }));
154
+ }
155
+
156
+ // Complex filter operations
157
+ async handleTempFilterChange<K extends keyof Filters>(
158
+ key: K,
159
+ value: Filters[K]
160
+ ): Promise<void> {
161
+ await this.setState(state => ({
162
+ ...state,
163
+ tempFilters: { ...state.tempFilters, [key]: value }
164
+ }));
165
+ }
166
+
167
+ async commitTempFilterChange<K extends keyof Filters>(
168
+ key: K,
169
+ defaultValue: Filters[K]
170
+ ): Promise<void> {
171
+ const state = await this.getState();
172
+ const value = state.tempFilters[key] ?? defaultValue;
173
+
174
+ await this.handleTempFilterChange(key, value);
175
+ await this.submitFilterUpdate();
176
+ }
177
+
178
+ async handleFilterCommitIndexDB(newFilters: Partial<Filters>): Promise<void> {
179
+ await this.setState(state => {
180
+ const apiParams: QueryParams = {
181
+ ...state.apiFilters,
182
+ availability: (newFilters.availability as string[]) || [],
183
+ bedrooms: newFilters.bedrooms || [],
184
+ cost: newFilters.cost || null,
185
+ highlights: newFilters.highlights || [],
186
+ };
187
+ return {
188
+ ...state,
189
+ filters: { ...state.filters, ...newFilters },
190
+ apiFilters: apiParams,
191
+ };
192
+ });
193
+ }
194
+
195
+ async commitAvailabilityChange(): Promise<void> {
196
+ await this.submitFilterUpdate();
197
+ }
198
+
199
+ async submitFilterUpdate(): Promise<void> {
200
+ await this.setState(state => {
201
+ try {
202
+ const apiParams: QueryParams = {
203
+ ...state.apiFilters,
204
+ availability: (state.filters.availability as string[]) || [],
205
+ bedrooms: state.filters.bedrooms || [],
206
+ cost: state.filters.cost || null,
207
+ highlights: state.filters.highlights || [],
208
+ };
209
+
210
+ // Note: updateParams function call would need to be handled in consuming app
211
+ // You might want to emit an event or use a callback for this
212
+
213
+ return {
214
+ ...state,
215
+ apiFilters: apiParams,
216
+ };
217
+ } catch (error) {
218
+ console.error("Error submitting filter update:", error);
219
+ return state;
220
+ }
221
+ });
222
+ }
223
+
224
+ // Persistence operations
225
+ async loadPersistedFilters(): Promise<void> {
226
+ // This method is now redundant since we're always loading from IndexedDB
227
+ // But we'll keep it for compatibility and mark filters as loaded
228
+ await this.setState(state => ({ ...state, filtersLoaded: true }));
229
+ }
230
+
231
+ // Utility methods
232
+ async getUnitData(unitId: string): Promise<UnitData | null> {
233
+ const state = await this.getState();
234
+ return state.data[unitId] ?? null;
235
+ }
236
+
237
+ async getFilters(): Promise<Filters> {
238
+ const state = await this.getState();
239
+ return state.filters;
240
+ }
241
+
242
+ async getTempFilters(): Promise<Filters> {
243
+ const state = await this.getState();
244
+ return state.tempFilters;
245
+ }
246
+
247
+ async getApiFilters(): Promise<QueryParams> {
248
+ const state = await this.getState();
249
+ return state.apiFilters;
250
+ }
251
+
252
+ async getFullState(): Promise<AppStoreData> {
253
+ return this.getState();
254
+ }
255
+
256
+ // Initialize with default values if needed
257
+ async initialize(): Promise<void> {
258
+ const state = await this.getState();
259
+ if (Object.keys(state.data).length === 0 && !state.filtersLoaded) {
260
+ await this.setState(state => ({
261
+ ...defaultAppStoreData,
262
+ ...state,
263
+ filtersLoaded: true
264
+ }));
265
+ }
266
+ }
267
+ }
268
+
269
+ // Export singleton instance
270
+ export const appStore = new AppStore();
@@ -0,0 +1,64 @@
1
+ // src/api/favourites.ts
2
+ import { getDB } from "../db";
3
+ import { ensureUser } from "./users";
4
+ import { FavoritesSchema } from "../schema";
5
+
6
+ const keyFor = (userId: string, propertyId: string) =>
7
+ `favorites:${userId}:${propertyId}`;
8
+
9
+ export async function getFavoritedUnitsForProperty(
10
+ propertyId: string | number,
11
+ ): Promise<string[]> {
12
+ const db = await getDB();
13
+ const { uuid } = await ensureUser();
14
+ const key = keyFor(uuid, String(propertyId));
15
+ const row = await db.kv.get(key);
16
+ const parsed = row ? FavoritesSchema.safeParse(row.value) : null;
17
+ return parsed?.success ? parsed.data.unitIds : [];
18
+ }
19
+
20
+ export async function setFavoriteUnit(
21
+ propertyId: string | number,
22
+ unitId: string,
23
+ on: boolean,
24
+ ): Promise<string[]> {
25
+ const db = await getDB();
26
+ const { uuid } = await ensureUser();
27
+ const key = keyFor(uuid, String(propertyId));
28
+
29
+ return db.transaction("rw", db.kv, async () => {
30
+ const row = await db.kv.get(key);
31
+ const current =
32
+ row && FavoritesSchema.safeParse(row.value).success
33
+ ? (row!.value as any)
34
+ : { unitIds: [] as string[], updatedAt: new Date().toISOString() };
35
+
36
+ const nextSet = new Set<string>(current.unitIds);
37
+ on ? nextSet.add(unitId) : nextSet.delete(unitId);
38
+
39
+ const next = {
40
+ unitIds: [...nextSet],
41
+ updatedAt: new Date().toISOString(),
42
+ };
43
+ const validated = FavoritesSchema.parse(next);
44
+ await db.kv.put({ key, value: validated });
45
+ return validated.unitIds;
46
+ });
47
+ }
48
+
49
+ export async function toggleFavoriteUnit(
50
+ propertyId: string | number,
51
+ unitId: string,
52
+ ): Promise<string[]> {
53
+ const current = await getFavoritedUnitsForProperty(propertyId);
54
+ const on = !current.includes(unitId);
55
+ return setFavoriteUnit(propertyId, unitId, on);
56
+ }
57
+
58
+ export async function isUnitFavorited(
59
+ propertyId: string | number,
60
+ unitId: string,
61
+ ): Promise<boolean> {
62
+ const list = await getFavoritedUnitsForProperty(propertyId);
63
+ return list.includes(unitId);
64
+ }