rajt 0.0.9 → 0.0.11

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 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.9",
4
+ "version": "0.0.11",
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
  },
@@ -24,7 +25,7 @@
24
25
  "sam:local": "sam local start-api --warm-containers LAZY --debug --template-file ../../template-dev.yaml",
25
26
  "sam:package": "sam package --template-file ../../template-prod.yaml --output-template-file ../../packaged.yaml",
26
27
  "sam:deploy": "sam deploy --template-file ../../packaged.yaml --stack-name rajt-llrt --capabilities CAPABILITY_IAM",
27
- "sam:update": "source ../../.env.prod && aws lambda update-function-code --function-name $AWS_NAME --zip-file fileb: //../../lambda.zip --region $AWS_REGION",
28
+ "sam:update": "source ../../.env.prod && aws lambda update-function-code --function-name $AWS_NAME --zip-file fileb://../../lambda.zip --region $AWS_REGION --no-cli-pager 2>&1 >/dev/null",
28
29
  "cache:routes": "tsx src/scripts/cache-routes.ts",
29
30
  "ensure-dirs": "rm -rf ../../dist ../../tmp && mkdir -p ../../tmp && chmod 755 ../../tmp && mkdir -p ../../dist && chmod 755 ../../dist",
30
31
  "clean": "rm -rf ../../dist ../../tmp",
@@ -34,6 +35,8 @@
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",
37
40
  "@hono/node-server": "^1.14.1",
38
41
  "@hono/zod-validator": "^0.4.3",
39
42
  "@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: '.env.dev' })
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,2 @@
1
+ export { Dynamodb } from './client'
2
+ export { Model, PartitionKey, SortKey } from './decorators'
@@ -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
@@ -2,7 +2,7 @@ import { config } from 'dotenv'
2
2
  import { handle } from 'hono/aws-lambda'
3
3
  import createApp from './create-app'
4
4
 
5
- config({ path: '.env.prod' })
5
+ config({ path: '../../.env.prod' })
6
6
 
7
7
  // @ts-ignore
8
8
  await import('../../../tmp/import-routes.mjs')