rajt 0.0.10 → 0.0.12
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/package.json +5 -1
- package/src/dev.ts +1 -1
- package/src/dynamodb/client.ts +13 -0
- package/src/dynamodb/decorators.ts +20 -0
- package/src/dynamodb/index.ts +2 -0
- package/src/dynamodb/metadata-registry.ts +28 -0
- package/src/dynamodb/model.ts +137 -0
- package/src/dynamodb/query-builder.ts +155 -0
- package/src/prod.ts +1 -1
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rajt",
|
|
3
3
|
"description": "A serverless bundler layer, fully typed for AWS Lambda (Node.js and LLRT) and Cloudflare Workers.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.12",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
|
+
"./dynamodb": "./src/dynamodb/index.ts",
|
|
9
10
|
"./http": "./src/http.ts",
|
|
10
11
|
"./types": "./src/types.ts"
|
|
11
12
|
},
|
|
@@ -34,6 +35,9 @@
|
|
|
34
35
|
"start": "node ../../dist/index.js"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
38
|
+
"@aws-lite/client": "^0.23.2",
|
|
39
|
+
"@aws-lite/dynamodb": "^0.3.9",
|
|
40
|
+
"@aws-lite/dynamodb-types": "^0.3.11",
|
|
37
41
|
"@hono/node-server": "^1.14.1",
|
|
38
42
|
"@hono/zod-validator": "^0.4.3",
|
|
39
43
|
"@types/node": "^20.11.0",
|
package/src/dev.ts
CHANGED
|
@@ -4,7 +4,7 @@ import createApp from './create-app'
|
|
|
4
4
|
import getRoutes from './routes'
|
|
5
5
|
import { getAvailablePort } from './utils/port'
|
|
6
6
|
|
|
7
|
-
config({ path: '
|
|
7
|
+
config({ path: '../../.env.dev' })
|
|
8
8
|
|
|
9
9
|
const routes = await getRoutes()
|
|
10
10
|
const fetch = createApp({ routes }).fetch
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import awsLite from '@aws-lite/client'
|
|
2
|
+
import dynamodbLite from '@aws-lite/dynamodb'
|
|
3
|
+
import AbstractModel from './model'
|
|
4
|
+
|
|
5
|
+
export const aws = await awsLite({
|
|
6
|
+
plugins: [dynamodbLite]
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export class Dynamodb {
|
|
10
|
+
static model<T extends object>(cls: new (...args: any[]) => T) {
|
|
11
|
+
return new AbstractModel<T>(cls, aws.DynamoDB)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { registerModelMetadata, registerKeyMetadata } from './metadata-registry'
|
|
2
|
+
|
|
3
|
+
export function Model(opt: string | { table: string }) {
|
|
4
|
+
return function (target: Function) {
|
|
5
|
+
const table = typeof opt === 'string' ? opt : opt.table
|
|
6
|
+
registerModelMetadata(target, { table })
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PartitionKey(attrName?: string) {
|
|
11
|
+
return function (target: any, propertyKey: string) {
|
|
12
|
+
registerKeyMetadata(target.constructor, 'PK', attrName || propertyKey)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SortKey(attrName?: string) {
|
|
17
|
+
return function (target: any, propertyKey: string) {
|
|
18
|
+
registerKeyMetadata(target.constructor, 'SK', attrName || propertyKey)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ModelMetadata = {
|
|
2
|
+
table: string,
|
|
3
|
+
keys: Record<'PK' | 'SK', string>,
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const modelRegistry = new Map<Function, ModelMetadata>()
|
|
7
|
+
|
|
8
|
+
export function registerModelMetadata(target: Function, metadata: { table: string }) {
|
|
9
|
+
const data = modelRegistry.get(target)
|
|
10
|
+
if (data)
|
|
11
|
+
data.table = metadata.table
|
|
12
|
+
else
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
modelRegistry.set(target, { table: metadata.table, keys: {} })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function registerKeyMetadata(target: Function, keyType: 'PK' | 'SK', name: string) {
|
|
18
|
+
const data = modelRegistry.get(target)
|
|
19
|
+
if (data)
|
|
20
|
+
data.keys[keyType] = name
|
|
21
|
+
else
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
modelRegistry.set(target, { table: '', keys: { [keyType]: name } })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getModelMetadata(target: Function) {
|
|
27
|
+
return modelRegistry.get(target)
|
|
28
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { AwsLiteDynamoDB } from '@aws-lite/dynamodb-types'
|
|
2
|
+
import { getModelMetadata } from './metadata-registry'
|
|
3
|
+
import type { ModelMetadata } from './metadata-registry'
|
|
4
|
+
import QueryBuilder from './query-builder'
|
|
5
|
+
|
|
6
|
+
export default class AbstractModel<T extends object> {
|
|
7
|
+
private meta: ModelMetadata
|
|
8
|
+
private lastKey?: Record<string, any>
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
cls: (new (...args: any[]) => T) | ModelMetadata,
|
|
12
|
+
private db: AwsLiteDynamoDB,
|
|
13
|
+
private queryBuilder?: QueryBuilder,
|
|
14
|
+
private model?: AbstractModel<T>
|
|
15
|
+
) {
|
|
16
|
+
if (typeof (cls as ModelMetadata).table === 'string') {
|
|
17
|
+
this.meta = cls as ModelMetadata
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
const meta = getModelMetadata(cls)
|
|
23
|
+
if (!meta)
|
|
24
|
+
throw new Error('Missing model metadata')
|
|
25
|
+
|
|
26
|
+
this.meta = meta
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get table(): string {
|
|
30
|
+
return this.meta.table
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get keys() {
|
|
34
|
+
return this.meta.keys
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set lastEvaluatedKey(val: Record<string, any> | undefined) {
|
|
38
|
+
if (this.model) {
|
|
39
|
+
this.model.lastKey = val
|
|
40
|
+
} else {
|
|
41
|
+
this.lastKey = val
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
get lastEvaluatedKey() {
|
|
45
|
+
return this.lastKey
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
schema(pk: string, sk?: string) {
|
|
49
|
+
const keys = { [this.meta.keys.PK]: pk }
|
|
50
|
+
if (sk && this.meta.keys.SK)
|
|
51
|
+
keys[this.meta.keys.SK] = sk
|
|
52
|
+
|
|
53
|
+
return keys
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
where(builderFn: (q: QueryBuilder) => void) {
|
|
57
|
+
const qb = new QueryBuilder()
|
|
58
|
+
builderFn(qb)
|
|
59
|
+
const model = new AbstractModel<T>(this.meta, this.db, qb, this)
|
|
60
|
+
return model
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async scan(filterFn?: (item: T) => boolean) {
|
|
64
|
+
const result = await this.db.Scan({ TableName: this.table, ...this.queryBuilder?.filters })
|
|
65
|
+
this.lastEvaluatedKey = result.LastEvaluatedKey
|
|
66
|
+
const items = result.Items as T[]
|
|
67
|
+
return filterFn ? items.filter(filterFn) : items
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async query(filterFn?: (item: T) => boolean) {
|
|
71
|
+
const result = await this.db.Query({ TableName: this.table, ...this.queryBuilder?.conditions })
|
|
72
|
+
this.lastEvaluatedKey = result.LastEvaluatedKey
|
|
73
|
+
const items = result.Items as T[]
|
|
74
|
+
return filterFn ? items.filter(filterFn) : items
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async get(pk: string, sk?: string) {
|
|
78
|
+
const result = await this.db.GetItem({ TableName: this.table, Key: this.schema(pk, sk) })
|
|
79
|
+
return result.Item as T
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async put(item: Partial<T>) {
|
|
83
|
+
await this.db.PutItem({ TableName: this.table, Item: item })
|
|
84
|
+
return item
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async update(key: string | [string, string], attrs: Partial<T>) {
|
|
88
|
+
const UpdateExpressionParts: string[] = []
|
|
89
|
+
const ExpressionAttributeValues: any = {}
|
|
90
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
91
|
+
UpdateExpressionParts.push(`#${k} = :${k}`)
|
|
92
|
+
ExpressionAttributeValues[`:${k}`] = v
|
|
93
|
+
}
|
|
94
|
+
const UpdateExpression = 'SET ' + UpdateExpressionParts.join(', ')
|
|
95
|
+
const ExpressionAttributeNames = Object.fromEntries(Object.keys(attrs).map(k => [`#${k}`, k]))
|
|
96
|
+
|
|
97
|
+
return this.db.UpdateItem({
|
|
98
|
+
TableName: this.table,
|
|
99
|
+
Key: Array.isArray(key) ? this.schema(key[0], key[1]) : this.schema(key),
|
|
100
|
+
UpdateExpression,
|
|
101
|
+
ExpressionAttributeValues,
|
|
102
|
+
ExpressionAttributeNames
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async delete(pk: string, sk?: string) {
|
|
107
|
+
return this.db.DeleteItem({ TableName: this.table, Key: this.schema(pk, sk) })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async batchGet(keys: Array<{ pk: string, sk?: string }>) {
|
|
111
|
+
const result = await this.db.BatchGetItem({ RequestItems: {
|
|
112
|
+
[this.table]: { Keys: keys.map(({ pk, sk }) => this.schema(pk, sk)) }
|
|
113
|
+
} })
|
|
114
|
+
return result.Responses?.[this.table] as T[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async batchWrite(items: Array<{ put?: Partial<T>, delete?: { pk: string, sk?: string } }>) {
|
|
118
|
+
const WriteRequests = items.map(i => {
|
|
119
|
+
if (i.put) {
|
|
120
|
+
return { PutRequest: { Item: i.put } }
|
|
121
|
+
} else if (i.delete) {
|
|
122
|
+
return { DeleteRequest: { Key: this.schema(i.delete.pk, i.delete.sk) } }
|
|
123
|
+
}
|
|
124
|
+
return null
|
|
125
|
+
}).filter(Boolean)
|
|
126
|
+
|
|
127
|
+
return this.db.BatchWriteItem({ RequestItems: {[this.table]: WriteRequests} })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async deleteMany(keys: Array<{ pk: string, sk?: string }>) {
|
|
131
|
+
return this.batchWrite(keys.map(k => ({ delete: k })))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async putMany(items: Array<Partial<T>>) {
|
|
135
|
+
return this.batchWrite(items.map(i => ({ put: i })))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
type Operator = '=' | '<>' | '<' | '<=' | '>' | '>=' | 'begins_with' | 'between' | 'in' | 'attribute_exists' | 'attribute_not_exists' | 'attribute_type' | 'contains' | 'size'
|
|
2
|
+
|
|
3
|
+
type Condition = {
|
|
4
|
+
type: 'filter' | 'keyCondition',
|
|
5
|
+
field: string,
|
|
6
|
+
operator: Operator,
|
|
7
|
+
value: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default class QueryBuilder {
|
|
11
|
+
private _conditions: Condition[] = []
|
|
12
|
+
private _limit?: number
|
|
13
|
+
private _startKey?: Record<string, any>
|
|
14
|
+
private _index?: string
|
|
15
|
+
|
|
16
|
+
filter(field: string, operator: Operator, value: any = null) {
|
|
17
|
+
this._conditions.push({ type: 'filter', field, operator, value })
|
|
18
|
+
return this
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
keyCondition(field: string, operator: Operator | any, value?: any) {
|
|
22
|
+
const noVal = value === undefined
|
|
23
|
+
this._conditions.push({ type: 'keyCondition', field, operator: noVal ? '=' : operator, value: noVal ? operator : value })
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
limit(n: number) {
|
|
28
|
+
this._limit = n
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exclusiveStartKey(key: Record<string, any>) {
|
|
33
|
+
this._startKey = key
|
|
34
|
+
return this
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
index(name: string) {
|
|
38
|
+
this._index = name
|
|
39
|
+
return this
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
buildExpression(type: 'filter' | 'keyCondition') {
|
|
43
|
+
const exprParts: string[] = []
|
|
44
|
+
const values: Record<string, any> = {}
|
|
45
|
+
const names: Record<string, string> = {}
|
|
46
|
+
|
|
47
|
+
let i = 0
|
|
48
|
+
for (const cond of this._conditions.filter(c => c.type === type)) {
|
|
49
|
+
const attr = `#attr${i}`
|
|
50
|
+
const val = `:val${i}`
|
|
51
|
+
names[attr] = cond.field
|
|
52
|
+
|
|
53
|
+
switch (cond.operator) {
|
|
54
|
+
case 'between': {
|
|
55
|
+
exprParts.push(`${attr} BETWEEN ${val}a AND ${val}b`)
|
|
56
|
+
values[`${val}a`] = cond.value[0]
|
|
57
|
+
values[`${val}b`] = cond.value[1]
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
case 'begins_with': {
|
|
61
|
+
exprParts.push(`begins_with(${attr}, ${val})`)
|
|
62
|
+
values[val] = cond.value
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
case 'in': {
|
|
66
|
+
const inVals = cond.value.map((v: any, j: number) => {
|
|
67
|
+
const vKey = `${val}_${j}`
|
|
68
|
+
values[vKey] = v
|
|
69
|
+
return vKey
|
|
70
|
+
})
|
|
71
|
+
exprParts.push(`${attr} IN (${inVals.join(', ')})`)
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
case 'attribute_exists': {
|
|
75
|
+
exprParts.push(`attribute_exists(${attr})`)
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
case 'attribute_not_exists': {
|
|
79
|
+
exprParts.push(`attribute_not_exists(${attr})`)
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
case 'attribute_type': {
|
|
83
|
+
exprParts.push(`attribute_type(${attr}, ${val})`)
|
|
84
|
+
values[val] = cond.value
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
case 'contains': {
|
|
88
|
+
exprParts.push(`contains(${attr}, ${val})`)
|
|
89
|
+
values[val] = cond.value
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
case 'size': {
|
|
93
|
+
exprParts.push(`size(${attr}) = ${val}`)
|
|
94
|
+
values[val] = cond.value
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
default: {
|
|
98
|
+
exprParts.push(`${attr} ${cond.operator} ${val}`)
|
|
99
|
+
values[val] = cond.value
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
expression: exprParts.length ? exprParts.join(' AND ') : undefined,
|
|
108
|
+
names: Object.keys(names).length ? names : undefined,
|
|
109
|
+
values: Object.keys(values).length ? values : undefined,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get filters() {
|
|
114
|
+
const filter = this.buildExpression('filter')
|
|
115
|
+
const params: any = {}
|
|
116
|
+
|
|
117
|
+
if (this._limit)
|
|
118
|
+
params.Limit = this._limit
|
|
119
|
+
|
|
120
|
+
if (this._startKey)
|
|
121
|
+
params.ExclusiveStartKey = this._startKey
|
|
122
|
+
|
|
123
|
+
if (filter.expression)
|
|
124
|
+
params.FilterExpression = filter.expression
|
|
125
|
+
|
|
126
|
+
if (filter.names)
|
|
127
|
+
params.ExpressionAttributeNames = filter.names
|
|
128
|
+
|
|
129
|
+
if (filter.values)
|
|
130
|
+
params.ExpressionAttributeValues = filter.values
|
|
131
|
+
|
|
132
|
+
return params
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get conditions() {
|
|
136
|
+
const keys = this.buildExpression('keyCondition')
|
|
137
|
+
const filters = this.filters
|
|
138
|
+
|
|
139
|
+
const params: any = { ...filters }
|
|
140
|
+
|
|
141
|
+
if (this._index)
|
|
142
|
+
params.IndexName = this._index
|
|
143
|
+
|
|
144
|
+
if (keys.expression)
|
|
145
|
+
params.KeyConditionExpression = keys.expression
|
|
146
|
+
|
|
147
|
+
if (keys.names || filters?.ExpressionAttributeNames)
|
|
148
|
+
params.ExpressionAttributeNames = { ...(keys?.names || {}), ...(filters?.ExpressionAttributeNames || {}) }
|
|
149
|
+
|
|
150
|
+
if (keys.values || filters?.ExpressionAttributeValues)
|
|
151
|
+
params.ExpressionAttributeValues = { ...(keys?.values || {}), ...(filters?.ExpressionAttributeValues || {}) }
|
|
152
|
+
|
|
153
|
+
return params
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/prod.ts
CHANGED