Skip to content

Commit 8b3c06a

Browse files
author
Miha J. Mulec
committed
fix: add autoincrement types
1 parent b7212d2 commit 8b3c06a

File tree

9 files changed

+136
-52
lines changed

9 files changed

+136
-52
lines changed

apps/mmstack/src/app/app.config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
withPreloading,
2222
} from '@angular/router';
2323
import { provideValidatorConfig } from '@mmstack/form-material';
24+
import { provideDBConfig } from '@mmstack/local';
2425
import {
2526
createCacheInterceptor,
2627
createDedupeRequestsInterceptor,
@@ -36,6 +37,20 @@ export const appConfig: ApplicationConfig = {
3637
provide: LOCALE_ID,
3738
useValue: 'en-US',
3839
},
40+
provideDBConfig({
41+
dbName: 'my-todo-app',
42+
version: 1,
43+
syncTabs: false, // Enable cross-tab synchronization
44+
schema: {
45+
tasks: {
46+
primaryKey: 'id',
47+
autoIncrement: true,
48+
// indexes: {
49+
// byStatus: { keyPath: 'status' },
50+
// },
51+
},
52+
},
53+
}),
3954
provideClientHydration(
4055
withEventReplay(),
4156
withHttpTransferCacheOptions({

apps/mmstack/src/app/app.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Component } from '@angular/core';
2+
import { TaskListComponent } from './todo.component';
23

34
@Component({
45
selector: 'app-root',
5-
imports: [],
6-
template: ``,
6+
imports: [TaskListComponent],
7+
template: `<app-task-list />`,
78
styles: ``,
89
})
910
export class App {}
1011

11-
1212
export function test() {
13-
return 'yay'
14-
}
13+
return 'yay';
14+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Component, effect } from '@angular/core';
2+
import { idb } from '@mmstack/local';
3+
4+
type Task = {
5+
id: number;
6+
title: string;
7+
status: 'pending' | 'completed';
8+
};
9+
10+
@Component({
11+
selector: 'app-task-list',
12+
template: `
13+
@if (tasks.isLoading()) {
14+
<p>Loading tasks...</p>
15+
}
16+
<ul>
17+
@for (task of tasks.value(); track task.id) {
18+
<li>{{ task.title }}</li>
19+
}
20+
</ul>
21+
<button (click)="addTask()">Add New Task</button>
22+
`,
23+
})
24+
export class TaskListComponent {
25+
// a call to 'tasks' will always return the same instance, so updates happen across the entire application
26+
tasks = idb<Task, 'id'>('tasks');
27+
28+
constructor() {
29+
effect(() => {
30+
console.log('Tasks in the database:', this.tasks.value());
31+
});
32+
}
33+
34+
async addTask() {
35+
await this.tasks.add({ title: 'A new adventure!', status: 'pending' });
36+
console.log('Task added successfully!');
37+
}
38+
}

packages/local/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ Use the `idb` helper function in any component or service to get a reactive hand
6565
```typescript
6666
// task-list.component.ts
6767

68-
type Task {
68+
type Task = {
6969
id?: number;
7070
title: string;
71-
status: 'pending' | 'completed'
72-
}
71+
status: 'pending' | 'completed';
72+
};
7373

7474
@Component({
7575
selector: 'app-task-list',
@@ -85,9 +85,9 @@ type Task {
8585
<button (click)="addTask()">Add New Task</button>
8686
`,
8787
})
88-
export class TaskListComponent {
89-
// a call to 'tasks' will always return the same instance, so updates happen across the entire application
90-
tasks = idb<Task>('tasks');
88+
export class TaskList {
89+
// a call to 'tasks' will always return the same instance, so updates happen across the entire application
90+
tasks = idb<Task, 'id'>('tasks');
9191

9292
constructor() {
9393
effect(() => {

packages/local/src/lib/idb/idb-client.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,13 @@ export type IDBClient = ResourceRef<IDBConnection> & {
4444
* @param opt Options for creating the table handler.
4545
* @returns An IDBTable handler for the specified table.
4646
*/
47-
useTable: <T extends Record<PropertyKey, any>>(
47+
useTable: <
48+
T extends Record<PropertyKey, any>,
49+
TKey extends keyof T = keyof T,
50+
>(
4851
tableName: string,
49-
opt: CreateIDBTableOptions<T>,
50-
) => IDBTable<T>;
52+
opt?: CreateIDBTableOptions<T>,
53+
) => IDBTable<T, TKey>;
5154
};
5255

5356
/** Options for creating the IDBClient */
@@ -116,7 +119,7 @@ export function createIDBClient(
116119

117120
const { onBlocked, onVersionChange, onClose } = lifeCycle ?? {};
118121

119-
const registry = new Map<string, IDBTable<any>>();
122+
const registry = new Map<string, IDBTable<any, any>>();
120123

121124
const events$ = dbEvents(dbName, version, injector);
122125

@@ -127,12 +130,15 @@ export function createIDBClient(
127130
const base = {
128131
name: dbName,
129132
version,
130-
useTable: <T extends Record<PropertyKey, any>>(
133+
useTable: <
134+
T extends Record<PropertyKey, any>,
135+
TKey extends keyof T = keyof T,
136+
>(
131137
tableName: string,
132-
options: CreateIDBTableOptions<T>,
133-
) => {
138+
options?: CreateIDBTableOptions<T>,
139+
): IDBTable<T, TKey> => {
134140
const found = registry.get(tableName);
135-
if (found) return found;
141+
if (found) return found as IDBTable<T, TKey>;
136142

137143
const tableSchema = schema[tableName];
138144

@@ -144,7 +150,7 @@ export function createIDBClient(
144150
).join(', ')}`,
145151
);
146152

147-
return createNoopTable<T>(tableName);
153+
return createNoopTable<T, TKey>(tableName);
148154
}
149155

150156
const eventFactory = createEventFactory<T, IDBValidKey>(
@@ -153,9 +159,9 @@ export function createIDBClient(
153159
tableName,
154160
);
155161

156-
const newTable = createNewTable<T>(tableName, {
162+
const newTable = createNewTable<T, TKey>(tableName, {
157163
...options,
158-
injector: options.injector ?? injector,
164+
injector: options?.injector ?? injector,
159165
schema: tableSchema as IDBTableSchema<T>,
160166
client: client,
161167
fireEvent: (e) => {

packages/local/src/lib/idb/idb-events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class IDBEventBus {
3030
readonly events$ = new Subject<IDBChangeEvent<any, any>>();
3131
}
3232

33-
function generateID() {
33+
export function generateID() {
3434
if (globalThis.crypto?.randomUUID) {
3535
return globalThis.crypto.randomUUID();
3636
}

packages/local/src/lib/idb/idb-table.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { filter, Observable } from 'rxjs';
1010
import type { IDBClient } from './idb-client';
1111
import type { IDBConnection } from './idb-connection';
12-
import type { FireEvent, IDBChangeEvent } from './idb-events';
12+
import { generateID, type FireEvent, type IDBChangeEvent } from './idb-events';
1313
import type { IDBTableSchema } from './idb-schema';
1414
import { toResourceObject } from './to-resource-object';
1515

@@ -26,17 +26,21 @@ export type CreateIDBTableOptions<T extends Record<PropertyKey, any>> = {
2626
equal?: ValueEqualityFn<T>;
2727
};
2828

29+
type AddValue<T, TKey extends keyof T> = Omit<T, TKey> & {
30+
[K in TKey]?: IDBValidKey;
31+
};
32+
2933
/**
3034
* Represents a table handler for IndexedDB.
3135
* It provides methods to add, update, and remove items,
3236
* as well as to retrieve the table's data.
3337
* It extends ResourceRef to provide reactive data handling.
3438
* All updates happen optimistcally and are reverted in case of an error.
3539
*/
36-
export type IDBTable<T extends Record<PropertyKey, any>> = Omit<
37-
ResourceRef<T[]>,
38-
'set' | 'update'
39-
> & {
40+
export type IDBTable<
41+
T extends Record<PropertyKey, any>,
42+
TKey extends keyof T,
43+
> = Omit<ResourceRef<T[]>, 'set' | 'update'> & {
4044
/**
4145
* The name of the table.
4246
*/
@@ -46,26 +50,26 @@ export type IDBTable<T extends Record<PropertyKey, any>> = Omit<
4650
* @param value The item to add.
4751
* @returns A promise that resolves when the add operation is complete.
4852
*/
49-
add: (value: T) => Promise<void>;
53+
add: (value: AddValue<T, TKey>) => Promise<T[TKey]>;
5054
/**
5155
*
5256
* @param key The key of the item to update.
5357
* @param itemOrUpdater The new item or a function that takes the previous item and returns the new item.
5458
* @returns A promise that resolves when the update is complete.
5559
*/
56-
update: (
57-
key: IDBValidKey,
58-
itemOrUpdater: T | ((prev: T) => T),
59-
) => Promise<void>;
60+
update: (key: T[TKey], itemOrUpdater: T | ((prev: T) => T)) => Promise<void>;
6061
/**
6162
* Removes an item from the table by its key.
6263
* @param key The key of the item to remove.
6364
* @returns A promise that resolves when the remove operation is complete.
6465
*/
65-
remove: (key: IDBValidKey) => Promise<void>;
66+
remove: (key: T[TKey]) => Promise<void>;
6667
};
6768

68-
export function createNewTable<T extends Record<PropertyKey, any>>(
69+
export function createNewTable<
70+
T extends Record<PropertyKey, any>,
71+
TKey extends keyof T,
72+
>(
6973
tableName: string,
7074
{
7175
client,
@@ -79,7 +83,7 @@ export function createNewTable<T extends Record<PropertyKey, any>>(
7983
fireEvent: FireEvent<T, IDBValidKey>;
8084
events$: Observable<IDBChangeEvent<T, IDBValidKey>>;
8185
},
82-
): IDBTable<T> {
86+
): IDBTable<T, TKey> {
8387
const rawTableData = toResourceObject(
8488
resource<T[], IDBConnection>({
8589
params: () => client.value(),
@@ -123,17 +127,35 @@ export function createNewTable<T extends Record<PropertyKey, any>>(
123127
},
124128
};
125129

126-
const add = async (item: T, fromEvent = false): Promise<void> => {
130+
const add = async (
131+
item: AddValue<T, TKey>,
132+
fromEvent = false,
133+
): Promise<T[TKey]> => {
127134
const prev = untracked(tableData.value);
128135

136+
let tempKey = (item as any)[schema.primaryKey] as T[TKey] | undefined;
137+
138+
if (schema.autoIncrement && tempKey === undefined && prev.length > 0) {
139+
const isString = typeof prev[0]?.[schema.primaryKey] === 'string';
140+
const isNumber = typeof prev[0]?.[schema.primaryKey] === 'number';
141+
if (isString) {
142+
tempKey = generateID() as T[TKey];
143+
} else if (isNumber) {
144+
tempKey = (Math.max(
145+
...prev.map((v) => v[schema.primaryKey] as number),
146+
) + 1) as T[TKey];
147+
}
148+
}
149+
129150
try {
130-
const tempKey = item[schema.primaryKey];
131-
tableData.update((cur) => [...cur, item]);
132-
let payload = item;
151+
tableData.update((cur) => [...cur, item as T]);
152+
153+
let payload = item as T;
154+
133155
if (!fromEvent) {
134156
const key = await untracked(client.value).add<T>(
135157
tableName,
136-
item,
158+
item as T,
137159
controller.signal,
138160
);
139161
if (key !== tempKey) {
@@ -146,11 +168,14 @@ export function createNewTable<T extends Record<PropertyKey, any>>(
146168
type: 'add',
147169
payload,
148170
});
171+
return key as T[TKey];
149172
}
173+
return tempKey as T[TKey];
150174
} catch (err) {
151175
if (isDevMode())
152176
console.error(`Error adding value to table ${tableName}:`, err);
153177
tableData.set(prev);
178+
return tempKey as T[TKey];
154179
}
155180
};
156181

@@ -225,9 +250,10 @@ export function createNewTable<T extends Record<PropertyKey, any>>(
225250
};
226251
}
227252

228-
export function createNoopTable<T extends Record<PropertyKey, any>>(
229-
tableName: string,
230-
): IDBTable<T> {
253+
export function createNoopTable<
254+
T extends Record<PropertyKey, any>,
255+
TKey extends keyof T,
256+
>(tableName: string): IDBTable<T, TKey> {
231257
const data = toResourceObject<T[]>(
232258
resource({
233259
loader: () => Promise.resolve([]),
@@ -239,7 +265,7 @@ export function createNoopTable<T extends Record<PropertyKey, any>>(
239265
return {
240266
...data,
241267
name: tableName,
242-
add: () => Promise.resolve(),
268+
add: () => Promise.resolve(Math.random() as T[TKey]),
243269
update: () => Promise.resolve(),
244270
remove: () => Promise.resolve(),
245271
};

packages/local/src/lib/idb/idb.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ export type CreateIDBOptions<T extends Record<PropertyKey, any>> = Omit<
110110
* users.add({ name: 'John Doe' });
111111
*```
112112
*/
113-
export function idb<T extends Record<PropertyKey, any>>(
114-
tableName: string,
115-
opt: CreateIDBTableOptions<T>,
116-
): IDBTable<T> {
113+
export function idb<
114+
T extends Record<PropertyKey, any>,
115+
TKey extends keyof T = keyof T,
116+
>(tableName: string, opt?: CreateIDBTableOptions<T>): IDBTable<T, TKey> {
117117
const client =
118-
opt.client ??
119-
(opt.injector
118+
opt?.client ??
119+
(opt?.injector
120120
? opt.injector.get(DefaultConnection).client
121121
: inject(DefaultConnection).client);
122122

tsconfig.base.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"moduleResolution": "node",
88
"emitDecoratorMetadata": true,
99
"experimentalDecorators": true,
10-
"noUncheckedIndexedAccess": true,
1110
"importHelpers": true,
1211
"target": "es2015",
1312
"module": "esnext",

0 commit comments

Comments
 (0)