Skip to content

Commit 5953698

Browse files
authored
Merge pull request #14 from prettygoodtech/feature/dot-notation
Add support for dot, bracket notations with a Proxy
2 parents faa4780 + 554ada1 commit 5953698

File tree

5 files changed

+197
-13
lines changed

5 files changed

+197
-13
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Build artifacts
22
lib
33
types
4+
coverage
45

56
# Documentation
67
*.md

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning][semver].
88
## [Unreleased]
99
### Added
1010
- "Features" section to README.
11+
- Support for dot and bracket property accessors.
1112

1213
### Changed
1314
- Upgrade `@ava/typescript` from `3.0.1` to `4.0.0`.

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@ easily, and safely. Built with [TypeScript][typescript].
1313
for Node.js.
1414
- Supports `setItem`, `getItem`, `deleteItem`, `clear`, and `key` methods.
1515
- Supports `length` property.
16-
- Small footprint.
16+
- Supports dot and bracket property accessors.
17+
- Small footprint with zero dependencies.
1718
- TypeScript declarations.
1819

19-
### Roadmap to v1
20-
- Support for dot notation property accessor.
21-
- Support for bracket notation property accessor.
22-
2320
## Installation and Usage
2421
First, install the package:
2522

@@ -40,6 +37,14 @@ nsStorage.setItem("preferred-theme", "dark");
4037

4138
// Will retrieve item with key my-prefix:last-accessed-on
4239
const lastAccessedOn = nsStorage.getItem("last-accessed-on");
40+
41+
// Dot notation, will use key my-prefix:favorite
42+
nsStorage.favorite = "sport-section";
43+
delete nsStorage.favorite;
44+
45+
// Bracket notation, will use key my-prefix:favorite
46+
nsStorage["favorite"] = "sport-section";
47+
delete nsStorage["favorite"];
4348
```
4449

4550
### Caveats

src/index.test.ts

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,26 @@ test.beforeEach((t) => {
3232
t.context.storage.clear();
3333
});
3434

35-
test("NamespacedStorage - constructor", (t) => {
35+
test.serial("NamespacedStorage - constructor", (t) => {
3636
t.notThrows(() => new NamespacedStorage(t.context.storage, "foo"));
3737
});
3838

39-
test("NamespacedStorage - setItem", (t) => {
39+
test.serial("NamespacedStorage - setItem", (t) => {
4040
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
4141
nsStorage.setItem("user-id", "1234");
4242

4343
t.is(t.context.storage.getItem("foo:user-id"), "1234");
4444
});
4545

46-
test("NamespacedStorage - getItem", (t) => {
46+
test.serial("NamespacedStorage - getItem", (t) => {
4747
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
4848
t.context.storage.setItem("foo:user-id", "1234");
4949
t.context.storage.setItem("user-id", "5678");
5050

5151
t.is(nsStorage.getItem("user-id"), "1234");
5252
});
5353

54-
test("NamespacedStorage - removeItem", (t) => {
54+
test.serial("NamespacedStorage - removeItem", (t) => {
5555
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
5656
t.context.storage.setItem("foo:user-id", "1234");
5757
t.context.storage.setItem("user-id", "5678");
@@ -62,7 +62,7 @@ test("NamespacedStorage - removeItem", (t) => {
6262
t.is(t.context.storage.getItem("user-id"), "5678");
6363
});
6464

65-
test("NamespacedStorage - clear", (t) => {
65+
test.serial("NamespacedStorage - clear", (t) => {
6666
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
6767
t.context.storage.setItem("foo:user-id", "1234");
6868
t.context.storage.setItem("foo:preferred-theme", "dark");
@@ -76,7 +76,7 @@ test("NamespacedStorage - clear", (t) => {
7676
t.is(t.context.storage.getItem("user-id"), "5678");
7777
});
7878

79-
test("NamespacedStorage - length", (t) => {
79+
test.serial("NamespacedStorage - length", (t) => {
8080
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
8181
t.context.storage.setItem("foo:user-id", "1234");
8282
t.context.storage.setItem("foo:preferred-theme", "dark");
@@ -85,7 +85,7 @@ test("NamespacedStorage - length", (t) => {
8585
t.is(nsStorage.length, 2);
8686
});
8787

88-
test("NamespacedStorage - key", (t) => {
88+
test.serial("NamespacedStorage - key", (t) => {
8989
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
9090
t.context.storage.setItem("foo:user-id", "1234"); // idx 0, ns_idx 0
9191
t.context.storage.setItem("user-id", "5678"); // idx 1, ns_idx -
@@ -94,3 +94,122 @@ test("NamespacedStorage - key", (t) => {
9494
t.is(nsStorage.key(0), "user-id");
9595
t.is(nsStorage.key(1), "preferred-theme");
9696
});
97+
98+
test.serial("NamespacedStorage - dot notation - assignment", (t) => {
99+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
100+
nsStorage.user_id = "1234";
101+
102+
t.is(t.context.storage.getItem("foo:user_id"), "1234");
103+
});
104+
105+
test.serial(
106+
"NamespacedStorage - dot notation - don't overwrite methods",
107+
(t) => {
108+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
109+
110+
// @ts-ignore, TypeScript already prevents overwriting methods because of type
111+
// mismatch, but a test is necessary for plain JS use.
112+
nsStorage.setItem = "1234";
113+
114+
t.is(typeof nsStorage.setItem, "function");
115+
}
116+
);
117+
118+
test.serial("NamespacedStorage - dot notation - access key/value pair", (t) => {
119+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
120+
t.context.storage.setItem("foo:user_id", "5678");
121+
122+
t.is(nsStorage.user_id, "5678");
123+
});
124+
125+
test.serial("NamespacedStorage - dot notation - access storage method", (t) => {
126+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
127+
128+
t.is(typeof nsStorage.setItem, "function");
129+
});
130+
131+
test.serial("NamespacedStorage - dot notation - delete key/value pair", (t) => {
132+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
133+
t.context.storage.setItem("foo:user_id", "5678");
134+
135+
// Check key/value pair is actually available
136+
t.is(nsStorage.user_id, "5678");
137+
delete nsStorage.user_id;
138+
t.is(nsStorage.user_id, null);
139+
});
140+
141+
test.serial(
142+
"NamespacedStorage - dot notation - don't delete storage method",
143+
(t) => {
144+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
145+
146+
// @ts-ignore, TypeScript already prevents deleting explicitly defined,
147+
// non-optional properties, but a test is necessary for plain JS use.
148+
delete nsStorage.setItem;
149+
t.is(typeof nsStorage.setItem, "function");
150+
}
151+
);
152+
153+
test.serial("NamespacedStorage - bracket notation - assignment", (t) => {
154+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
155+
nsStorage["user_id"] = "1234";
156+
157+
t.is(t.context.storage.getItem("foo:user_id"), "1234");
158+
});
159+
160+
test.serial(
161+
"NamespacedStorage - bracket notation - don't overwrite methods",
162+
(t) => {
163+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
164+
165+
// @ts-ignore, TypeScript already prevents overwriting methods because of type
166+
// mismatch, but a test is necessary for plain JS use.
167+
nsStorage["setItem"] = "1234";
168+
169+
t.is(typeof nsStorage["setItem"], "function");
170+
}
171+
);
172+
173+
test.serial(
174+
"NamespacedStorage - bracket notation - access key/value pair",
175+
(t) => {
176+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
177+
t.context.storage.setItem("foo:user_id", "5678");
178+
179+
t.is(nsStorage["user_id"], "5678");
180+
}
181+
);
182+
183+
test.serial(
184+
"NamespacedStorage - bracket notation - access storage method",
185+
(t) => {
186+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
187+
188+
t.is(typeof nsStorage["setItem"], "function");
189+
}
190+
);
191+
192+
test.serial(
193+
"NamespacedStorage - bracket notation - delete key/value pair",
194+
(t) => {
195+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
196+
t.context.storage.setItem("foo:user_id", "5678");
197+
198+
// Check key/value pair is actually available
199+
t.is(nsStorage["user_id"], "5678");
200+
delete nsStorage["user_id"];
201+
t.is(nsStorage["user_id"], null);
202+
}
203+
);
204+
205+
test.serial(
206+
"NamespacedStorage - bracket notation - don't delete storage method",
207+
(t) => {
208+
const nsStorage = new NamespacedStorage(t.context.storage, "foo");
209+
210+
// @ts-ignore, TypeScript already prevents deleting explicitly defined,
211+
// non-optional properties, but a test is necessary for plain JS use.
212+
delete nsStorage["setItem"];
213+
t.is(typeof nsStorage["setItem"], "function");
214+
}
215+
);

src/index.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,73 @@ const isKeyPrefixed = (key: string, prefix: string): boolean => {
3030
return key.startsWith(prefix + PREFIX_SEPARATOR);
3131
};
3232

33+
const RESERVED_PROPERTIES = [
34+
// Storage properties
35+
"length",
36+
"key",
37+
"setItem",
38+
"getItem",
39+
"removeItem",
40+
"clear",
41+
// Private properties
42+
"storage",
43+
"prefix",
44+
// Private custom methods
45+
"getNamespacedEntries",
46+
];
47+
48+
const propertyAccessorHandler: ProxyHandler<NamespacedStorage> = {
49+
// Necessary for:
50+
// - nsStorage.foo = "bar"
51+
// - nsStorage["foo"] = "bar"
52+
set: (target, property: string, value: string) => {
53+
if (RESERVED_PROPERTIES.includes(property)) {
54+
return true;
55+
}
56+
57+
target.setItem(property, value);
58+
return true;
59+
},
60+
61+
// Necessary for:
62+
// - const foo = nsStorage.foo
63+
// - const foo = nsStorage["foo"]
64+
get: (target, property: string): string | null => {
65+
if (RESERVED_PROPERTIES.includes(property)) {
66+
return target[property];
67+
}
68+
69+
return target.getItem(property);
70+
},
71+
72+
// Necessary for:
73+
// - delete nsStorage.foo
74+
// - delete nsStorage["foo"]
75+
deleteProperty: (target, property: string): boolean => {
76+
if (RESERVED_PROPERTIES.includes(property)) {
77+
// Storage seems to return true when attempting to delete a reserved
78+
// property, even if this property is never really removed.
79+
return true;
80+
}
81+
82+
target.removeItem(property);
83+
return true;
84+
},
85+
};
86+
3387
export class NamespacedStorage implements Storage {
88+
[key: string]: any;
89+
3490
/**
3591
* Creates a new instance of `NamespacedStorage` based on the given
3692
* `storage` implementation.
3793
*
3894
* All methods will operate on the given `storage` object using the given
3995
* `prefix` to create a namespace for keys.
4096
*/
41-
constructor(private storage: Storage, private prefix: string) {}
97+
constructor(private storage: Storage, private prefix: string) {
98+
return new Proxy(this, propertyAccessorHandler);
99+
}
42100

43101
/** Returns the number of key/value pairs in the namespace. */
44102
public get length(): number {

0 commit comments

Comments
 (0)