Skip to content

Commit dc2015f

Browse files
committed
Added support for multi-attribute values
1 parent b523fe7 commit dc2015f

File tree

11 files changed

+460
-220
lines changed

11 files changed

+460
-220
lines changed

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,12 @@
3434
"watch": "make watch"
3535
},
3636
"dependencies": {
37-
"flat": "^4.1.0",
3837
"lodash": "^4.17.10",
3938
"squel": "^5.12.2"
4039
},
4140
"devDependencies": {
4241
"@types/aws-lambda": "^8.10.8",
4342
"@types/chance": "^1.0.0",
44-
"@types/flat": "0.0.28",
4543
"@types/jasmine": "^2.8.6",
4644
"@types/lodash": "^4.14.113",
4745
"@types/node": "^9.6.2",

src/isotopes/client/index.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*/
2222

2323
import { SimpleDB } from "aws-sdk"
24-
import { toPairs } from "lodash"
24+
import { castArray, toPairs } from "lodash"
2525

2626
import { IsotopeDictionary } from "../format"
2727

@@ -63,6 +63,51 @@ const defaultOptions: IsotopeClientOptions = {
6363
consistent: false
6464
}
6565

66+
/* ----------------------------------------------------------------------------
67+
* Functions
68+
* ------------------------------------------------------------------------- */
69+
70+
/**
71+
* Map a dictionary to a list of SimpleDB attributes
72+
*
73+
* @param attrs - Attributes
74+
*
75+
* @return SimpleDB attributes
76+
*/
77+
export function mapDictionaryToAttributes(
78+
attrs: IsotopeDictionary
79+
): SimpleDB.ReplaceableAttribute[] {
80+
return toPairs(attrs)
81+
.reduce<SimpleDB.ReplaceableAttribute[]>((list, [key, value]) => [
82+
...list,
83+
...castArray(value).map(entry => ({
84+
Name: key,
85+
Value: entry,
86+
Replace: true
87+
}))
88+
], [])
89+
}
90+
91+
/**
92+
* Map a list of SimpleDB attributes to a dictionary
93+
*
94+
* @param attrs - Attributes
95+
*
96+
* @return Dictionary
97+
*/
98+
export function mapAttributesToDictionary(
99+
attrs: SimpleDB.Attribute[]
100+
): IsotopeDictionary {
101+
return attrs
102+
.reduce<IsotopeDictionary>((dict, { Name, Value }) => ({
103+
...dict,
104+
...(Name.match(/\[\]$/)
105+
? { [Name]: [ ...(dict[Name] || []), Value ] }
106+
: { [Name]: Value }
107+
)
108+
}), {})
109+
}
110+
66111
/* ----------------------------------------------------------------------------
67112
* Class
68113
* ------------------------------------------------------------------------- */
@@ -115,10 +160,7 @@ export class IsotopeClient {
115160
/* Map identifier and attributes */
116161
return {
117162
id,
118-
attrs: Attributes
119-
.reduce<IsotopeDictionary>((attrs, { Name, Value }) => ({
120-
...attrs, [Name]: Value
121-
}), {})
163+
attrs: mapAttributesToDictionary(Attributes)
122164
}
123165
}
124166

@@ -136,12 +178,7 @@ export class IsotopeClient {
136178
await this.simpledb.putAttributes({
137179
DomainName: this.domain,
138180
ItemName: id,
139-
Attributes: toPairs(attrs)
140-
.map<SimpleDB.Attribute>(([key, value]) => ({
141-
Name: key,
142-
Value: value,
143-
Replace: true
144-
}))
181+
Attributes: mapDictionaryToAttributes(attrs)
145182
}).promise()
146183
}
147184

@@ -193,10 +230,7 @@ export class IsotopeClient {
193230
return {
194231
items: Items.map<IsotopeClientItem>(({ Name: id, Attributes }) => ({
195232
id,
196-
attrs: Attributes
197-
.reduce<IsotopeDictionary>((attrs, { Name, Value }) => ({
198-
...attrs, [Name]: Value
199-
}), {})
233+
attrs: mapAttributesToDictionary(Attributes)
200234
})),
201235
next: NextToken
202236
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (c) 2018 Martin Donath <[email protected]>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to
6+
* deal in the Software without restriction, including without limitation the
7+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8+
* sell copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20+
* IN THE SOFTWARE.
21+
*/
22+
23+
/* ----------------------------------------------------------------------------
24+
* Types
25+
* ------------------------------------------------------------------------- */
26+
27+
/**
28+
* Isotope value
29+
*/
30+
export type IsotopeValue = string | number | boolean | object
31+
32+
/* ------------------------------------------------------------------------- */
33+
34+
/**
35+
* Isotope format encoding for values
36+
*/
37+
export type IsotopeFormatEncoding =
38+
| "json" /* Default JSON encoding */
39+
| "text" /* Strings are encoded as literals */
40+
41+
/* ----------------------------------------------------------------------------
42+
* Functions
43+
* ------------------------------------------------------------------------- */
44+
45+
/**
46+
* Encode a value using the given encoding
47+
*
48+
* By default every field is formatted as JSON in order to ensure type
49+
* information not to be lost. However, this means that strings are actually
50+
* double-quoted which may not be desirable if the SimpleDB domain is also used
51+
* by other parts of your system as it cripples the querying experience.
52+
*
53+
* This library provides the ability to use an alternate encoding and store
54+
* strings as literals so they are written without quotes. When decoding the
55+
* data, we intercept JSON.parse assuming that we encountered a literal string
56+
* if it fails to decode. However, this imposes the following limitations:
57+
*
58+
* 1. Numbers that are encoded as strings (e.g. house numbers, because they can
59+
* exhibit values as "2A" etc.) are interpreted as numbers when decoded with
60+
* JSON.parse. Countermeasure: ensure that numbers are typed as numbers, or
61+
* string fields contain at least one non-number character.
62+
*
63+
* 2. If strings accidentally contain valid JSON, e.g. "{}", the value is parsed
64+
* as JSON and the field gets assigned that precise value. This also breaks
65+
* type safety. Countermeasure: ensure that your strings are never valid JSON
66+
* by prepending some character that makes JSON.parse fail.
67+
*
68+
* As enforcing as those restrictions may seem to be, it is often true that
69+
* the properties and characteristics of the data are known a-priori and those
70+
* special cases can be ruled out with great certainty. This also means that
71+
* querying the data from other parts of your system gets easier as string
72+
* values don't need to be enclosed into quotes (and don't start thinking about
73+
* LIKE queries) which is far more user-friendly.
74+
*
75+
* @internal
76+
*
77+
* @param value - Value to encode
78+
* @param encoding - Encoding
79+
*
80+
* @return Encoded value
81+
*/
82+
export function encode(
83+
value: IsotopeValue, encoding: IsotopeFormatEncoding
84+
): string {
85+
return encoding === "json" || typeof value !== "string"
86+
? JSON.stringify(value)
87+
: value
88+
}
89+
90+
/**
91+
* Decode a value using the given encoding
92+
*
93+
* See the function documentation on flatten() for a detailed explanation on
94+
* how error handling is implemented and why.
95+
*
96+
* @internal
97+
*
98+
* @param value - Value to decode
99+
* @param encoding - Encoding
100+
*
101+
* @return Decoded value
102+
*/
103+
export function decode(
104+
value: string, encoding: IsotopeFormatEncoding
105+
): IsotopeValue {
106+
try {
107+
return JSON.parse(value)
108+
} catch (err) {
109+
if (encoding === "text")
110+
return value
111+
throw err
112+
}
113+
}

0 commit comments

Comments
 (0)