forj 0.0.2 → 0.0.3
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 +7 -3
- package/src/d1/model.ts +11 -12
- package/src/d1/types.ts +7 -0
- package/src/dynamodb/client.ts +125 -0
- package/src/dynamodb/compact.ts +205 -0
- package/src/dynamodb/decorators.ts +126 -0
- package/src/dynamodb/index.ts +4 -0
- package/src/dynamodb/model.ts +258 -0
- package/src/dynamodb/query-builder.ts +174 -0
- package/src/dynamodb/repository.ts +31 -0
- package/src/dynamodb/schema.ts +107 -0
- package/src/dynamodb/types.ts +32 -0
- package/src/index.ts +1 -1
- package/src/model.ts +1 -1
- package/src/query-builder.ts +1 -5
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forj",
|
|
3
3
|
"description": "SQLite ORM and Query Builder whitout dependencies",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"files": ["src"],
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
|
-
"./d1": "./src/d1/index.ts"
|
|
10
|
+
"./d1": "./src/d1/index.ts",
|
|
11
|
+
"./d1/types": "./src/d1/types.ts"
|
|
11
12
|
},
|
|
12
13
|
"-exports": {
|
|
13
14
|
".": {
|
|
@@ -25,12 +26,15 @@
|
|
|
25
26
|
"-prepublishOnly": "bun run build"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@
|
|
29
|
+
"@aws-sdk/client-dynamodb": "3.817.0",
|
|
30
|
+
"@aws-sdk/lib-dynamodb": "3.817.0",
|
|
29
31
|
"pluralize": "^8.0",
|
|
32
|
+
"t0n": "^0.1",
|
|
30
33
|
"zod": "^3.19.1"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
36
|
"@types/pluralize": "^0.0.33",
|
|
37
|
+
"@cloudflare/workers-types": "^4.20260113.0",
|
|
34
38
|
"bun-types": "^1.2.13",
|
|
35
39
|
"terser": "^5.46.0",
|
|
36
40
|
"tiny-glob": "^0.2",
|
package/src/d1/model.ts
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
D1Database,
|
|
3
|
-
D1PreparedStatement,
|
|
4
|
-
D1Result, D1ExecResult, D1Meta,
|
|
5
|
-
} from '
|
|
3
|
+
// D1PreparedStatement,
|
|
4
|
+
// D1Result, D1ExecResult, D1Meta,
|
|
5
|
+
} from './types'
|
|
6
6
|
|
|
7
7
|
import z from 'zod'
|
|
8
|
+
import { Envir } from 't0n'
|
|
8
9
|
|
|
9
|
-
import
|
|
10
|
-
import
|
|
10
|
+
import QueryBuilder from '../query-builder'
|
|
11
|
+
import BModel from '../model'
|
|
11
12
|
import type {
|
|
12
13
|
DBSchema, SchemaKeys,
|
|
13
14
|
Item,
|
|
14
15
|
Pipe, Result, RunFn,
|
|
15
16
|
} from '../types'
|
|
16
17
|
|
|
17
|
-
export function
|
|
18
|
+
export function Model<
|
|
18
19
|
TSchema extends DBSchema,
|
|
19
20
|
TBase extends SchemaKeys<TSchema>
|
|
20
21
|
>(schema: TSchema, base: TBase) {
|
|
21
22
|
type S = z.infer<typeof schema>
|
|
22
|
-
return class extends
|
|
23
|
+
return class extends BaseModel<TBase, S> {
|
|
23
24
|
static $table = String(base)
|
|
24
25
|
static $schema = schema
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
export abstract class Model<TB extends keyof DB, DB> extends BaseModel<TB, DB> {
|
|
29
|
+
export abstract class BaseModel<TB extends keyof DB, DB> extends BModel<TB, DB> {
|
|
31
30
|
static $db: string | D1Database = 'DB'
|
|
32
31
|
|
|
33
32
|
static pipe<S, T>(): Pipe<S, T> {
|
|
@@ -40,11 +39,11 @@ export abstract class Model<TB extends keyof DB, DB> extends BaseModel<TB, DB> {
|
|
|
40
39
|
|
|
41
40
|
static DB() {
|
|
42
41
|
if (typeof this.$db == 'string') { // TODO: improv compatibility without nodejs_compat
|
|
43
|
-
if (!(this.$db
|
|
42
|
+
if (!Envir.has(this.$db))
|
|
44
43
|
throw new Error(`Database '${this.$db}' instance not provided.`)
|
|
45
44
|
|
|
46
45
|
// @ts-ignore
|
|
47
|
-
return
|
|
46
|
+
return Envir.get(this.$db) as D1Database
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
return this.$db
|
package/src/d1/types.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
+
import {
|
|
3
|
+
DynamoDBDocumentClient,
|
|
4
|
+
|
|
5
|
+
BatchGetCommand,
|
|
6
|
+
BatchWriteCommand,
|
|
7
|
+
|
|
8
|
+
DeleteCommand,
|
|
9
|
+
GetCommand,
|
|
10
|
+
PutCommand,
|
|
11
|
+
QueryCommand,
|
|
12
|
+
ScanCommand,
|
|
13
|
+
UpdateCommand,
|
|
14
|
+
|
|
15
|
+
ScanCommandInput,
|
|
16
|
+
QueryCommandInput,
|
|
17
|
+
UpdateCommandInput,
|
|
18
|
+
|
|
19
|
+
BatchGetCommandInput,
|
|
20
|
+
BatchWriteCommandInput,
|
|
21
|
+
} from '@aws-sdk/lib-dynamodb'
|
|
22
|
+
import type { NativeAttributeValue } from '@aws-sdk/util-dynamodb'
|
|
23
|
+
import AbstractModel from './model'
|
|
24
|
+
import { Keys, KeySchema } from './types'
|
|
25
|
+
|
|
26
|
+
const client = new DynamoDBClient(process.env?.AWS_SAM_LOCAL ? {
|
|
27
|
+
region: process.env.AWS_REGION || 'us-east-1',
|
|
28
|
+
endpoint: process.env.AWS_ENDPOINT_URL || undefined,
|
|
29
|
+
credentials: {
|
|
30
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'DUMMYIDEXAMPLE',
|
|
31
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'DUMMYEXAMPLEKEY',
|
|
32
|
+
},
|
|
33
|
+
} : {})
|
|
34
|
+
|
|
35
|
+
export const DocumentClient = DynamoDBDocumentClient.from(client)
|
|
36
|
+
|
|
37
|
+
export class Dynamodb {
|
|
38
|
+
static model<T extends object>(cls: new (...args: any[]) => T) {
|
|
39
|
+
return new AbstractModel<T>(cls)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static raw() {
|
|
43
|
+
return RawClient
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class RawClient {
|
|
48
|
+
static async get(TableName: string, key: Keys | Record<string, string>, sk?: string) {
|
|
49
|
+
return DocumentClient.send(new GetCommand({ TableName, Key: this.key(key, sk) }))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static async scan(TableName: string, filters: Omit<ScanCommandInput, 'TableName'>) {
|
|
53
|
+
return DocumentClient.send(new ScanCommand({ ...filters, TableName }))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static async query(TableName: string, filters: Omit<QueryCommandInput, 'TableName'>) {
|
|
57
|
+
return DocumentClient.send(new QueryCommand({ ...filters, TableName }))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static async put(TableName: string, Item: Record<string, NativeAttributeValue>) {
|
|
61
|
+
return DocumentClient.send(new PutCommand({ TableName, Item }))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static async update(
|
|
65
|
+
TableName: string,
|
|
66
|
+
filters: Omit<UpdateCommandInput, 'TableName' | 'Key'>,
|
|
67
|
+
key: Keys | Record<string, string>,
|
|
68
|
+
sk?: string
|
|
69
|
+
) {
|
|
70
|
+
return DocumentClient.send(new UpdateCommand({
|
|
71
|
+
...filters, TableName, Key: this.key(key, sk),
|
|
72
|
+
}))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static async delete(TableName: string, key: Keys | Record<string, string>, sk?: string) {
|
|
76
|
+
return DocumentClient.send(new DeleteCommand({ TableName, Key: this.key(key, sk) }))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static async batchGet(batch: BatchGetCommandInput) {
|
|
80
|
+
return DocumentClient.send(new BatchGetCommand(batch))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static async batchWrite(batch: BatchWriteCommandInput) {
|
|
84
|
+
return DocumentClient.send(new BatchWriteCommand(batch))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static key(
|
|
88
|
+
key: Keys | Record<string, string>,
|
|
89
|
+
sk?: string,
|
|
90
|
+
schema?: KeySchema,
|
|
91
|
+
defaultSK?: string,
|
|
92
|
+
) {
|
|
93
|
+
let pk: string
|
|
94
|
+
let skValue: string | undefined
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(key)) {
|
|
97
|
+
pk = key[0]
|
|
98
|
+
skValue = key[1] ?? sk
|
|
99
|
+
} else if (typeof key == 'object' && key != null) {
|
|
100
|
+
return key
|
|
101
|
+
} else {
|
|
102
|
+
pk = key
|
|
103
|
+
skValue = sk
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!schema) {
|
|
107
|
+
const keys = {PK: pk}
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
if (skValue) keys.SK = skValue
|
|
110
|
+
return keys
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const keys = { [schema.PK]: pk }
|
|
114
|
+
|
|
115
|
+
if (schema?.SK) {
|
|
116
|
+
if (skValue) {
|
|
117
|
+
keys[schema.SK] = skValue
|
|
118
|
+
} else if (defaultSK) {
|
|
119
|
+
keys[schema.SK] = defaultSK
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return keys
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { getLength } from 't0n'
|
|
2
|
+
import { isArraySchema } from './schema'
|
|
3
|
+
import type { SchemaStructure } from './types'
|
|
4
|
+
|
|
5
|
+
export default class Compact {
|
|
6
|
+
static #typeRegex: RegExp
|
|
7
|
+
static #reverseTypeRegex: RegExp
|
|
8
|
+
static #reverseTypeMap: Record<string, string>
|
|
9
|
+
static #typeMap: Record<string, string> = {
|
|
10
|
+
// Boolean
|
|
11
|
+
'true': 'T',
|
|
12
|
+
'false': 'F',
|
|
13
|
+
// Null
|
|
14
|
+
'null': 'N',
|
|
15
|
+
// Array
|
|
16
|
+
'[]': 'A',
|
|
17
|
+
'["0"]': 'A0',
|
|
18
|
+
'["1"]': 'A1',
|
|
19
|
+
'["false"]': 'A2',
|
|
20
|
+
'["true"]': 'A3',
|
|
21
|
+
// Object
|
|
22
|
+
'{}': 'O',
|
|
23
|
+
// String
|
|
24
|
+
'""': 'S',
|
|
25
|
+
'"0"': 'S0',
|
|
26
|
+
'"1"': 'S1',
|
|
27
|
+
'"false"': 'S2',
|
|
28
|
+
'"true"': 'S3',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static {
|
|
32
|
+
const keys = []
|
|
33
|
+
const values = []
|
|
34
|
+
const reverseTypeMap: Record<string, string> = {}
|
|
35
|
+
for (const key in this.#typeMap) {
|
|
36
|
+
const val = this.#typeMap[key]
|
|
37
|
+
const k = key.replace(/"/g, "'")
|
|
38
|
+
keys.push(k)
|
|
39
|
+
values.push(val)
|
|
40
|
+
|
|
41
|
+
reverseTypeMap[val] = k
|
|
42
|
+
this.#typeMap[k] = val
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.#reverseTypeMap = reverseTypeMap
|
|
46
|
+
this.#typeRegex = this.#mapRegex(keys)
|
|
47
|
+
this.#reverseTypeRegex = this.#mapRegex(values)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static encode(obj: any, schema: SchemaStructure): string {
|
|
51
|
+
const seen: any[] = []
|
|
52
|
+
|
|
53
|
+
return this.#pack(
|
|
54
|
+
JSON.stringify(this.zip(obj, schema, seen))
|
|
55
|
+
.replace(/"\^(\d+)"/g, '^$1')
|
|
56
|
+
.replace(/"/g, '~TDQ~')
|
|
57
|
+
.replace(/'/g, '"')
|
|
58
|
+
.replace(/~TDQ~/g, "'")
|
|
59
|
+
.replace(/\\'/g, "^'")
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static smartDecode<T = any>(val: any, schema: SchemaStructure): T {
|
|
64
|
+
if (!val) return val as T
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(val)) // @ts-ignore
|
|
67
|
+
return val.map((i: {v: string}) => this.decode<T>(i?.V, schema)).filter(Boolean) as T
|
|
68
|
+
|
|
69
|
+
return val?.V ? this.decode<T>(val.V, schema) : val
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static decode<T = any>(val: string, schema: SchemaStructure): T {
|
|
73
|
+
if (!val || typeof val != 'string') return val as T
|
|
74
|
+
|
|
75
|
+
return this.withSchema(this.unzip(JSON.parse(
|
|
76
|
+
this.#unpack(val)
|
|
77
|
+
.replace(/"/g, '~TSQ~')
|
|
78
|
+
.replace(/'/g, '"')
|
|
79
|
+
.replace(/~TSQ~/g, "'")
|
|
80
|
+
.replace(/\^"/g, '\\"')
|
|
81
|
+
.replace(/(?<=[,{\[]|^)(\^\d+)(?=[,\]}[]|$)/g, '"$1"')
|
|
82
|
+
)), schema) as T
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static zip(obj: any, schema: SchemaStructure, seen: any[]): any[] {
|
|
86
|
+
if (Array.isArray(obj))
|
|
87
|
+
return obj?.length ? obj.map(item => this.zip(item, schema, seen)) : []
|
|
88
|
+
|
|
89
|
+
if (this.#cantZip(obj)) return obj
|
|
90
|
+
|
|
91
|
+
return schema.map(key => {
|
|
92
|
+
if (typeof key == 'string')
|
|
93
|
+
return this.memo(obj[key], seen)
|
|
94
|
+
|
|
95
|
+
const mainKey = Object.keys(key)[0]
|
|
96
|
+
const subKeys = key[mainKey]
|
|
97
|
+
const val = obj[mainKey]
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(val))
|
|
100
|
+
return val.map(item => this.zip(item, subKeys, seen))
|
|
101
|
+
|
|
102
|
+
return this.zip(val, subKeys, seen)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static unzip(val: any, seen: any[] = []): any[] {
|
|
107
|
+
if (Array.isArray(val))
|
|
108
|
+
return val?.length ? val.map(item => this.unzip(item, seen)) : []
|
|
109
|
+
|
|
110
|
+
const type = typeof val
|
|
111
|
+
const length = getLength(val, type)
|
|
112
|
+
|
|
113
|
+
if (this.#cantZip(val, type, length)) return val
|
|
114
|
+
|
|
115
|
+
if (type == 'object') {
|
|
116
|
+
for (const key in val)
|
|
117
|
+
val[key] = this.unzip(val[key], seen)
|
|
118
|
+
|
|
119
|
+
return val
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (type == 'string' && val.startsWith('^')) {
|
|
123
|
+
const item = seen[parseInt(val.slice(1), 10)]
|
|
124
|
+
return item ? item : val
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
seen.push(val)
|
|
128
|
+
return val
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static withSchema(value: any[], keys: any[], deep = false): any {
|
|
132
|
+
if (!value || !Array.isArray(value))
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
if (!deep && isArraySchema(keys))
|
|
136
|
+
return value?.length ? value.map(v => this.withSchema(v, keys, true)) : []
|
|
137
|
+
|
|
138
|
+
return Object.fromEntries(
|
|
139
|
+
keys.map((key, index) => this.entry(key, value[index])).filter(Boolean)
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static entry(key: any, value: any): any {
|
|
144
|
+
if (!key) return undefined
|
|
145
|
+
|
|
146
|
+
if (typeof key == 'string')
|
|
147
|
+
return [key, value == null ? null : value]
|
|
148
|
+
|
|
149
|
+
const mainKey = Object.keys(key)[0]
|
|
150
|
+
const subKeys = key[mainKey]
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
if (value.length < 1)
|
|
154
|
+
return [mainKey, []]
|
|
155
|
+
|
|
156
|
+
return Array.isArray(value[0])
|
|
157
|
+
? [mainKey, value.map(v => this.withSchema(v, subKeys))]
|
|
158
|
+
: [mainKey, this.withSchema(value, subKeys)]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return [mainKey, value == null ? null : value]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static memo(val: any, seen: any[]): any {
|
|
165
|
+
if (Array.isArray(val))
|
|
166
|
+
return val.map(item => this.memo(item, seen))
|
|
167
|
+
|
|
168
|
+
const type = typeof val
|
|
169
|
+
if (type == 'object' && val != null) {
|
|
170
|
+
for (const key in val)
|
|
171
|
+
val[key] = this.memo(val[key], seen)
|
|
172
|
+
|
|
173
|
+
return val
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const length = getLength(val, type)
|
|
177
|
+
if (this.#cantZip(val, type, length)) return val
|
|
178
|
+
|
|
179
|
+
const index = seen.indexOf(val)
|
|
180
|
+
if (index != -1)
|
|
181
|
+
return `^${index}`
|
|
182
|
+
|
|
183
|
+
seen.push(val)
|
|
184
|
+
return val
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static #mapRegex(keys: string[]) {
|
|
188
|
+
keys = keys.sort((a, b) => b.length - a.length).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
189
|
+
return new RegExp(`(?<![^\\s,\\[\\{:])(${keys.join('|')})(?![^\\s,\\]\\}:])`, 'g')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static #pack(val: string): string {
|
|
193
|
+
return val.replace(this.#typeRegex, match => this.#typeMap[match])
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
static #unpack(val: string): string {
|
|
197
|
+
return val.replace(this.#reverseTypeRegex, match => this.#reverseTypeMap[match])
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static #cantZip(val: any, type: string = '', length: number = 0) {
|
|
201
|
+
if (!val || [null, true, false, 'true', 'false'].includes(val)) return true
|
|
202
|
+
|
|
203
|
+
return !type && !length ? false : type != 'object' && length < 2
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import pluralize from 'pluralize'
|
|
2
|
+
import type { ModelMetadata, ModelOpts } from './types'
|
|
3
|
+
|
|
4
|
+
export function getModelMetadata(target: Function | any): ModelMetadata {
|
|
5
|
+
if (!target?.m)
|
|
6
|
+
throw Error(`Entity "${target?.name}" not registred, Use @Entity or @Model.`)
|
|
7
|
+
|
|
8
|
+
const typeKeys = typeof target.m[1]
|
|
9
|
+
return {
|
|
10
|
+
table: target.m[0],
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
keys: typeKeys != 'undefined' ? (typeKeys == 'string' ? { PK: target.m[1] } : { PK: target.m[1][0], SK: target.m[1][1] }) : undefined,
|
|
13
|
+
defaultSK: target?.defaultSK || undefined,
|
|
14
|
+
zip: target.m[2] || false,
|
|
15
|
+
fields: target.m[3] || [],
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _table(target: Function | any, opt?: ModelOpts) {
|
|
20
|
+
if (!target?.m) target.m = []
|
|
21
|
+
const table = opt ? (typeof opt == 'string' ? opt : opt?.table) : undefined
|
|
22
|
+
|
|
23
|
+
target.m[0] = table || pluralize(target.name.toLocaleUpperCase())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _zip(target: Function | any) {
|
|
27
|
+
if (!target?.m) target.m = []
|
|
28
|
+
target.m[2] = true
|
|
29
|
+
target.m[3] = target?.schema || Object.keys(new target)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _key(target: Function | any, pk: string, sk?: string) {
|
|
33
|
+
if (!target?.m) target.m = []
|
|
34
|
+
target.m[1] = pk && sk ? [pk, sk] : [pk]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function _model(target: any, opt?: ModelOpts) {
|
|
38
|
+
_table(target, opt)
|
|
39
|
+
const notStr = typeof opt != 'string'
|
|
40
|
+
|
|
41
|
+
if (!opt || !notStr || (opt?.zip == null || opt?.zip))
|
|
42
|
+
_zip(target)
|
|
43
|
+
|
|
44
|
+
const pk = opt && notStr ? opt?.partitionKey : undefined
|
|
45
|
+
const sk = opt && notStr ? opt?.sortKey : undefined
|
|
46
|
+
_key(target, pk || 'PK', sk || 'SK')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _pk(target: any, prop: string) {
|
|
50
|
+
if (!target?.m) target.m = []
|
|
51
|
+
if (['string', 'undefined'].includes(typeof target.m[1])) {
|
|
52
|
+
target.m[1] = prop
|
|
53
|
+
} else {
|
|
54
|
+
target.m[1][0] = prop
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _sk(target: any, prop: string) {
|
|
59
|
+
if (!target?.m) target.m = []
|
|
60
|
+
if (['string', 'undefined'].includes(typeof target.m[1])) {
|
|
61
|
+
target.m[1] = []
|
|
62
|
+
target.m[1][1] = prop
|
|
63
|
+
} else {
|
|
64
|
+
target.m[1][0] = prop
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Entity(target: Function): void
|
|
69
|
+
export function Entity(opt?: ModelOpts): ClassDecorator
|
|
70
|
+
export function Entity(...args: any[]): void | ClassDecorator {
|
|
71
|
+
if (args.length == 1 && typeof args[0] == 'function')
|
|
72
|
+
return _table(args[0])
|
|
73
|
+
|
|
74
|
+
return (target: any) => _table(target, ...args)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function Model(target: Function): void
|
|
78
|
+
export function Model(opt?: ModelOpts): ClassDecorator
|
|
79
|
+
export function Model(...args: any[]): void | ClassDecorator {
|
|
80
|
+
if (args.length == 1 && typeof args[0] == 'function')
|
|
81
|
+
return _model(args[0])
|
|
82
|
+
|
|
83
|
+
return (target: any) => _model(target, ...args)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function Zip(target: Function): void
|
|
87
|
+
export function Zip(): ClassDecorator
|
|
88
|
+
export function Zip(...args: any[]): void | ClassDecorator {
|
|
89
|
+
if (args.length == 1 && typeof args[0] == 'function')
|
|
90
|
+
return _zip(args[0])
|
|
91
|
+
|
|
92
|
+
return (target: any) => _zip(target)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function Key(pk: string, sk?: string) {
|
|
96
|
+
return (target: any) => {
|
|
97
|
+
_key(target, pk, sk)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export const Keys = Key
|
|
101
|
+
|
|
102
|
+
export function PartitionKey(attrName: string): PropertyDecorator
|
|
103
|
+
export function PartitionKey(target: any, propertyKey: string): void
|
|
104
|
+
export function PartitionKey(target: any, propertyKey: string | undefined, parameterIndex: number): void
|
|
105
|
+
export function PartitionKey(...args: any[]): void | PropertyDecorator {
|
|
106
|
+
if (!args.length) return
|
|
107
|
+
|
|
108
|
+
if (typeof args[0] == 'function' && typeof args[1] == 'string' && args[1])
|
|
109
|
+
return _pk(args[0], args[1])
|
|
110
|
+
|
|
111
|
+
if (args.length == 1 && args[0])
|
|
112
|
+
return (target: any) => _pk(target, args[0])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function SortKey(attrName: string): PropertyDecorator
|
|
116
|
+
export function SortKey(target: any, propertyKey: string): void
|
|
117
|
+
export function SortKey(target: any, propertyKey: string | undefined, parameterIndex: number): void
|
|
118
|
+
export function SortKey(...args: any[]): void | PropertyDecorator {
|
|
119
|
+
if (!args.length) return
|
|
120
|
+
|
|
121
|
+
if (typeof args[0] == 'function' && typeof args[1] == 'string' && args[1])
|
|
122
|
+
return _sk(args[0], args[1])
|
|
123
|
+
|
|
124
|
+
if (args.length == 1 && args[0])
|
|
125
|
+
return (target: any) => _sk(target, args[0])
|
|
126
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { ModelMetadata, Keys, Model, Filter } from './types'
|
|
2
|
+
import { getModelMetadata } from './decorators'
|
|
3
|
+
import QueryBuilder from './query-builder'
|
|
4
|
+
import Compact from './compact'
|
|
5
|
+
import { RawClient } from './client'
|
|
6
|
+
import { isArraySchema } from './schema'
|
|
7
|
+
import { getLength } from 't0n'
|
|
8
|
+
|
|
9
|
+
export default class AbstractModel<T extends object> {
|
|
10
|
+
#meta: ModelMetadata
|
|
11
|
+
cls?: Model<T>
|
|
12
|
+
lastKey?: Record<string, any>
|
|
13
|
+
#queryBuilder?: QueryBuilder
|
|
14
|
+
#model?: AbstractModel<T>
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
cls: Model<T> | ModelMetadata,
|
|
18
|
+
queryBuilder?: QueryBuilder,
|
|
19
|
+
model?: AbstractModel<T>
|
|
20
|
+
) {
|
|
21
|
+
this.#queryBuilder = queryBuilder
|
|
22
|
+
this.#model = model
|
|
23
|
+
|
|
24
|
+
if (typeof (cls as ModelMetadata).table == 'string') {
|
|
25
|
+
this.#meta = cls as ModelMetadata
|
|
26
|
+
this.cls = model?.cls
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const meta = getModelMetadata(cls)
|
|
31
|
+
if (!meta)
|
|
32
|
+
throw new Error('Missing model metadata')
|
|
33
|
+
|
|
34
|
+
this.#meta = meta
|
|
35
|
+
this.cls = cls as Model<T>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get table(): string {
|
|
39
|
+
return this.#meta.table
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get keySchema() {
|
|
43
|
+
return this.#meta.keys
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set lastEvaluatedKey(val: Record<string, any> | undefined) {
|
|
47
|
+
if (this.#model) {
|
|
48
|
+
this.#model.lastKey = val
|
|
49
|
+
} else {
|
|
50
|
+
this.lastKey = val
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
get lastEvaluatedKey() {
|
|
54
|
+
return this.lastKey
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
where(builderFn: (q: QueryBuilder) => void) {
|
|
58
|
+
const qb = new QueryBuilder()
|
|
59
|
+
builderFn(qb)
|
|
60
|
+
return new AbstractModel<T>(this.#meta, qb, this)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async scan(filterFn?: Filter<T>) {
|
|
64
|
+
const result = await RawClient.scan(this.table, this.#queryBuilder?.filters)
|
|
65
|
+
|
|
66
|
+
this.lastEvaluatedKey = result.LastEvaluatedKey
|
|
67
|
+
return this.#processItems(result.Items, filterFn)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async query(filterFn?: Filter<T>) {
|
|
71
|
+
const result = await RawClient.query(this.table, this.#queryBuilder?.conditions)
|
|
72
|
+
|
|
73
|
+
this.lastEvaluatedKey = result.LastEvaluatedKey
|
|
74
|
+
return this.#processItems(result.Items, filterFn)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async get(key: Keys, sk?: string) {
|
|
78
|
+
const result = await RawClient.get(this.table, this.#key(key, sk))
|
|
79
|
+
return result.Item ? this.#processItem(result.Item) : undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async put(item: Partial<T>, key: Keys) {
|
|
83
|
+
let keys
|
|
84
|
+
if (this.#meta.zip) {
|
|
85
|
+
keys = this.#getItemKey(item, key)
|
|
86
|
+
this.#validateKeys(keys)
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
item = { ...keys, V: Compact.encode(this.#getItemWithoutKeys(item), this.#meta.fields) }
|
|
89
|
+
} else {
|
|
90
|
+
this.#validateKeys(item)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await RawClient.put(this.table, item)
|
|
94
|
+
return this.#processItem(item, keys)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async update(attrs: Partial<T>, key: Keys) {
|
|
98
|
+
let keys
|
|
99
|
+
if (this.#meta.zip) {
|
|
100
|
+
keys = this.#getItemKey(attrs, key)
|
|
101
|
+
this.#validateKeys(keys)
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
attrs = { V: Compact.encode(this.#getItemWithoutKeys(attrs), this.#meta.fields) }
|
|
104
|
+
} else {
|
|
105
|
+
this.#validateKeys(attrs)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const UpdateExpressionParts: string[] = []
|
|
109
|
+
const ExpressionAttributeValues: any = {}
|
|
110
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
111
|
+
UpdateExpressionParts.push(`#${k} = :${k}`)
|
|
112
|
+
ExpressionAttributeValues[`:${k}`] = v
|
|
113
|
+
}
|
|
114
|
+
const UpdateExpression = 'SET ' + UpdateExpressionParts.join(', ')
|
|
115
|
+
const ExpressionAttributeNames = Object.fromEntries(Object.keys(attrs).map(k => [`#${k}`, k]))
|
|
116
|
+
|
|
117
|
+
await RawClient.update(this.table, {
|
|
118
|
+
UpdateExpression,
|
|
119
|
+
ExpressionAttributeValues,
|
|
120
|
+
ExpressionAttributeNames,
|
|
121
|
+
}, this.#key(key))
|
|
122
|
+
|
|
123
|
+
return this.#processItem(attrs, keys)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async delete(key: Keys, sk?: string) {
|
|
127
|
+
return RawClient.delete(this.table, this.#key(key, sk))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async batchGet(keys: Array<Keys>) {
|
|
131
|
+
const result = await RawClient.batchGet({
|
|
132
|
+
RequestItems: { [this.table]: { Keys: keys.map(key => this.#key(key)) } },
|
|
133
|
+
})
|
|
134
|
+
return (result.Responses?.[this.table] as T[] || []).map(item => this.#processItem(item))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async batchWrite(items: Array<{ put?: Partial<T>, delete?: Keys }>) {
|
|
138
|
+
const WriteRequests = items.map(i => {
|
|
139
|
+
if (i.put) {
|
|
140
|
+
return { PutRequest: { Item: i.put } }
|
|
141
|
+
} else if (i.delete) {
|
|
142
|
+
return { DeleteRequest: { Key: this.#key(i.delete) } }
|
|
143
|
+
}
|
|
144
|
+
return null
|
|
145
|
+
}).filter(Boolean) as any[]
|
|
146
|
+
|
|
147
|
+
return RawClient.batchWrite({ RequestItems: { [this.table]: WriteRequests } })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async deleteMany(keys: Array<Keys>) {
|
|
151
|
+
return this.batchWrite(keys.map(k => ({ delete: k })))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async putMany(items: Array<Partial<T>>) {
|
|
155
|
+
return this.batchWrite(items.map(item => ({ put: item })))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#key(key: Keys, sk?: string) {
|
|
159
|
+
if (!this.#meta.keys) return {}
|
|
160
|
+
return RawClient.key(key, sk, this.#meta.keys, this.#meta.defaultSK)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#getItemKey(item: Partial<T>, key?: Keys): Record<string, string> {
|
|
164
|
+
if (!this.#meta.keys) return {}
|
|
165
|
+
|
|
166
|
+
const keys: Record<string, string> = {}
|
|
167
|
+
if (key)
|
|
168
|
+
this.#processExplicitKey(keys, key)
|
|
169
|
+
else if (getLength(item) > 0)
|
|
170
|
+
this.#processItemKeys(keys, item)
|
|
171
|
+
|
|
172
|
+
return keys
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#processExplicitKey(keys: Record<string, string>, key: Keys): void {
|
|
176
|
+
if (!this.#meta.keys) return
|
|
177
|
+
if (Array.isArray(key)) {
|
|
178
|
+
keys[this.#meta.keys.PK] = key[0]
|
|
179
|
+
|
|
180
|
+
if (this.#meta.keys?.SK) {
|
|
181
|
+
if (key.length > 1)
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
keys[this.#meta.keys.SK] = key[1]
|
|
184
|
+
else if (this.#meta.defaultSK)
|
|
185
|
+
keys[this.#meta.keys.SK] = this.#meta.defaultSK
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
keys[this.#meta.keys.PK] = String(key)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#processItemKeys(keys: Record<string, string>, item: Partial<T>): void {
|
|
193
|
+
if (!this.#meta.keys) return
|
|
194
|
+
|
|
195
|
+
const pkValue = item[this.#meta.keys.PK as keyof Partial<T>]
|
|
196
|
+
if (pkValue != null)
|
|
197
|
+
keys[this.#meta.keys.PK] = String(pkValue)
|
|
198
|
+
|
|
199
|
+
if (this.#meta.keys?.SK) {
|
|
200
|
+
const skValue = item[this.#meta.keys.SK as keyof Partial<T>]
|
|
201
|
+
if (skValue != null)
|
|
202
|
+
keys[this.#meta.keys.SK] = String(skValue)
|
|
203
|
+
else if (this.#meta.defaultSK)
|
|
204
|
+
keys[this.#meta.keys.SK] = this.#meta.defaultSK
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#validateKeys(keys: Record<string, any>) {
|
|
209
|
+
if (!this.#meta.keys)
|
|
210
|
+
throw new Error(`Missing keys of table "${this.table}"`)
|
|
211
|
+
|
|
212
|
+
if (!(this.#meta.keys.PK in keys))
|
|
213
|
+
throw new Error(`Missing partition key of table "${this.table}" `)
|
|
214
|
+
|
|
215
|
+
if (this.#meta.keys?.SK && !(this.#meta.keys.SK in keys))
|
|
216
|
+
throw new Error(`Missing sort key of table "${this.table}"`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#getItemWithoutKeys(item: Partial<T>): Partial<T> {
|
|
220
|
+
if (Array.isArray(item))
|
|
221
|
+
return item?.length ? item.map(i => this.#getItemWithoutKeys(i)) as T : [] as T
|
|
222
|
+
|
|
223
|
+
if (!this.#meta.keys || !item) return { ...item }
|
|
224
|
+
|
|
225
|
+
const { PK, SK } = this.#meta.keys
|
|
226
|
+
const { [PK as keyof T]: _, [SK as keyof T]: __, ...rest } = item
|
|
227
|
+
|
|
228
|
+
return rest as Partial<T>
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#processItems(items: any[] | undefined, filterFn?: Filter<T>): T[] {
|
|
232
|
+
if (!items || !items.length) return []
|
|
233
|
+
items = items.map(item => this.#processItem(item))
|
|
234
|
+
return filterFn ? items.filter(filterFn) : items
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#processItem(item: any, keys?: Record<string, string>): T {
|
|
238
|
+
if (this.#meta.zip && item?.V) {
|
|
239
|
+
const value = Compact.decode<T>(item.V, this.#meta.fields)
|
|
240
|
+
const model = isArraySchema(this.#meta.fields) && Array.isArray(value)
|
|
241
|
+
? value.map(v => new this.cls!(v))
|
|
242
|
+
: new this.cls!(value)
|
|
243
|
+
|
|
244
|
+
if (!keys) keys = this.#getItemKey(item)
|
|
245
|
+
|
|
246
|
+
return this.#withKey(model as T, keys)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return new this.cls!(item)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#withKey(model: T, keys: Record<string, string>): T {
|
|
253
|
+
// @ts-ignore
|
|
254
|
+
if (Array.isArray(model)) return model.map(m => this.#withKey(m, keys))
|
|
255
|
+
// @ts-ignore
|
|
256
|
+
return model.withKey(keys[this.#meta.keys.PK], keys[this.#meta.keys.SK] || undefined)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Condition, Operator } from './types'
|
|
2
|
+
|
|
3
|
+
export default class QueryBuilder {
|
|
4
|
+
#filters: Condition[] = []
|
|
5
|
+
#keyConditions: Condition[] = []
|
|
6
|
+
#limit?: number
|
|
7
|
+
#startKey?: Record<string, any>
|
|
8
|
+
#index?: string
|
|
9
|
+
#attrCounter = 1
|
|
10
|
+
#valCounter = 1
|
|
11
|
+
#fieldAttrMap: Record<string, string> = {}
|
|
12
|
+
#fieldValMap: Record<string, string> = {}
|
|
13
|
+
|
|
14
|
+
filter(field: string, operator: Operator, value: any = null) {
|
|
15
|
+
this.#filters.push({ type: 'filter', field, operator, value })
|
|
16
|
+
return this
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
keyCondition(field: string, operator: Operator | any, value?: any) {
|
|
20
|
+
const noVal = value == null
|
|
21
|
+
this.#keyConditions.push({ type: 'keyCondition', field, operator: noVal ? '=' : operator, value: noVal ? operator : value })
|
|
22
|
+
return this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
limit(n: number) {
|
|
26
|
+
this.#limit = n
|
|
27
|
+
return this
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exclusiveStartKey(key: Record<string, any>) {
|
|
31
|
+
this.#startKey = key
|
|
32
|
+
return this
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
index(name: string) {
|
|
36
|
+
this.#index = name
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private attrName(field: string) {
|
|
41
|
+
if (!this.#fieldAttrMap[field])
|
|
42
|
+
this.#fieldAttrMap[field] = '#a'+ this.#attrCounter++
|
|
43
|
+
|
|
44
|
+
return this.#fieldAttrMap[field]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private valName(val: any) {
|
|
48
|
+
val = String(val)
|
|
49
|
+
if (!this.#fieldValMap[val])
|
|
50
|
+
this.#fieldValMap[val] = ':v'+ this.#valCounter++
|
|
51
|
+
|
|
52
|
+
return this.#fieldValMap[val]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#resetCounters() {
|
|
56
|
+
this.#attrCounter = 0
|
|
57
|
+
this.#valCounter = 0
|
|
58
|
+
this.#fieldAttrMap = {}
|
|
59
|
+
this.#fieldValMap = {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
buildExpression(conditions: Condition[]) {
|
|
63
|
+
const exprParts: string[] = []
|
|
64
|
+
const values: Record<string, any> = {}
|
|
65
|
+
const names: Record<string, string> = {}
|
|
66
|
+
|
|
67
|
+
for (const cond of conditions) {
|
|
68
|
+
const attr = this.attrName(cond.field)
|
|
69
|
+
const val = Array.isArray(cond.value) ? '' : this.valName(cond.value)
|
|
70
|
+
names[attr] = cond.field
|
|
71
|
+
|
|
72
|
+
switch (cond.operator) {
|
|
73
|
+
case 'between': {
|
|
74
|
+
const val0 = this.valName(cond.value[0])
|
|
75
|
+
const val1 = this.valName(cond.value[1])
|
|
76
|
+
exprParts.push(`${attr} BETWEEN ${val0} AND ${val1}`)
|
|
77
|
+
values[val0] = cond.value[0]
|
|
78
|
+
values[val1] = cond.value[1]
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
case 'begins_with': {
|
|
82
|
+
exprParts.push(`begins_with(${attr}, ${val})`)
|
|
83
|
+
values[val] = cond.value
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
case 'in': {
|
|
87
|
+
const inVals = cond.value.map((v: any) => {
|
|
88
|
+
const key = this.valName(v)
|
|
89
|
+
values[key] = v
|
|
90
|
+
return key
|
|
91
|
+
})
|
|
92
|
+
exprParts.push(`${attr} IN (${inVals.join(', ')})`)
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
case 'attribute_exists': {
|
|
96
|
+
exprParts.push(`attribute_exists(${attr})`)
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case 'attribute_not_exists': {
|
|
100
|
+
exprParts.push(`attribute_not_exists(${attr})`)
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
case 'attribute_type': {
|
|
104
|
+
exprParts.push(`attribute_type(${attr}, ${val})`)
|
|
105
|
+
values[val] = cond.value
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
case 'contains': {
|
|
109
|
+
exprParts.push(`contains(${attr}, ${val})`)
|
|
110
|
+
values[val] = cond.value
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
case 'size': {
|
|
114
|
+
exprParts.push(`size(${attr}) = ${val}`)
|
|
115
|
+
values[val] = cond.value
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
default: {
|
|
119
|
+
exprParts.push(`${attr} ${cond.operator} ${val}`)
|
|
120
|
+
values[val] = cond.value
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
expression: exprParts.length ? exprParts.join(' AND ') : undefined,
|
|
127
|
+
names: Object.keys(names).length ? names : undefined,
|
|
128
|
+
values: Object.keys(values).length ? values : undefined,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get filters() {
|
|
133
|
+
const filter = this.buildExpression(this.#filters)
|
|
134
|
+
const params: any = {}
|
|
135
|
+
|
|
136
|
+
if (this.#limit)
|
|
137
|
+
params.Limit = this.#limit
|
|
138
|
+
|
|
139
|
+
if (this.#startKey)
|
|
140
|
+
params.ExclusiveStartKey = this.#startKey
|
|
141
|
+
|
|
142
|
+
if (filter.expression)
|
|
143
|
+
params.FilterExpression = filter.expression
|
|
144
|
+
|
|
145
|
+
if (filter.names)
|
|
146
|
+
params.ExpressionAttributeNames = filter.names
|
|
147
|
+
|
|
148
|
+
if (filter.values)
|
|
149
|
+
params.ExpressionAttributeValues = filter.values
|
|
150
|
+
|
|
151
|
+
return params
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get conditions() {
|
|
155
|
+
const keys = this.buildExpression(this.#keyConditions)
|
|
156
|
+
const filters = this.filters
|
|
157
|
+
|
|
158
|
+
const params: any = { ...filters }
|
|
159
|
+
|
|
160
|
+
if (this.#index)
|
|
161
|
+
params.IndexName = this.#index
|
|
162
|
+
|
|
163
|
+
if (keys.expression)
|
|
164
|
+
params.KeyConditionExpression = keys.expression
|
|
165
|
+
|
|
166
|
+
if (keys.names || filters?.ExpressionAttributeNames)
|
|
167
|
+
params.ExpressionAttributeNames = { ...(keys?.names || {}), ...(filters?.ExpressionAttributeNames || {}) }
|
|
168
|
+
|
|
169
|
+
if (keys.values || filters?.ExpressionAttributeValues)
|
|
170
|
+
params.ExpressionAttributeValues = { ...(keys?.values || {}), ...(filters?.ExpressionAttributeValues || {}) }
|
|
171
|
+
|
|
172
|
+
return params
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z, ZodTypeAny } from 'zod'
|
|
2
|
+
import { Dynamodb } from './client'
|
|
3
|
+
import { Schema } from './schema'
|
|
4
|
+
import { _model } from './decorators'
|
|
5
|
+
import type { ModelOpts } from './types'
|
|
6
|
+
|
|
7
|
+
export function Repository<
|
|
8
|
+
S extends ZodTypeAny,
|
|
9
|
+
B extends new (...args: any[]) => any
|
|
10
|
+
>(
|
|
11
|
+
schema: S,
|
|
12
|
+
base?: B | ModelOpts,
|
|
13
|
+
opts?: ModelOpts
|
|
14
|
+
) {
|
|
15
|
+
const isClass = typeof base == 'function'
|
|
16
|
+
type M = z.infer<S>
|
|
17
|
+
|
|
18
|
+
const Repo = Schema(schema, isClass ? base : undefined)
|
|
19
|
+
_model(Repo, isClass ? opts : base)
|
|
20
|
+
|
|
21
|
+
return class extends Repo {
|
|
22
|
+
static model = Dynamodb.model<M>(Repo as any)
|
|
23
|
+
|
|
24
|
+
static get lastKey() {
|
|
25
|
+
return this.model?.lastEvaluatedKey || null
|
|
26
|
+
}
|
|
27
|
+
} as unknown as (typeof Repo) & {
|
|
28
|
+
new (...args: any[]): InstanceType<typeof Repo>
|
|
29
|
+
model: ReturnType<typeof Dynamodb.model<M>>
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z, ZodTypeAny } from 'zod'
|
|
2
|
+
import type { SchemaStructure } from './types'
|
|
3
|
+
|
|
4
|
+
const m = Symbol('a')
|
|
5
|
+
export function isArraySchema(v: any): boolean {
|
|
6
|
+
return v[m] || false
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function arraySchema(v: any): any {
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
v[m] = true
|
|
12
|
+
return v
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function extractZodKeys(schema: ZodTypeAny): SchemaStructure {
|
|
16
|
+
if (schema instanceof z.ZodObject) {
|
|
17
|
+
return Object.entries(schema.shape).map(([key, value]) => {
|
|
18
|
+
const inner = unwrap(value as ZodTypeAny)
|
|
19
|
+
|
|
20
|
+
if (inner instanceof z.ZodObject)
|
|
21
|
+
return notEmpty(key, extractZodKeys(inner))
|
|
22
|
+
|
|
23
|
+
if (inner instanceof z.ZodArray) {
|
|
24
|
+
const item = unwrap(inner._def.type as ZodTypeAny)
|
|
25
|
+
return item instanceof z.ZodObject ? notEmpty(key, extractZodKeys(item)) : key
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return key
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (schema instanceof z.ZodArray) {
|
|
33
|
+
const item = unwrap(schema._def.type as ZodTypeAny)
|
|
34
|
+
if (item instanceof z.ZodObject)
|
|
35
|
+
return arraySchema(extractZodKeys(item))
|
|
36
|
+
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function unwrap(schema: ZodTypeAny): ZodTypeAny {
|
|
44
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable)
|
|
45
|
+
return unwrap(schema._def.innerType)
|
|
46
|
+
|
|
47
|
+
if (schema instanceof z.ZodDefault)
|
|
48
|
+
return unwrap(schema._def.innerType)
|
|
49
|
+
|
|
50
|
+
// if (schema instanceof z.ZodUnion)
|
|
51
|
+
// return unwrap(schema._def.options[0] as ZodTypeAny)
|
|
52
|
+
|
|
53
|
+
if (schema instanceof z.ZodUnion) {
|
|
54
|
+
const options = schema._def.options as ZodTypeAny[]
|
|
55
|
+
const nonEmptyOption = options.find(opt => !(opt instanceof z.ZodUndefined) && !(opt instanceof z.ZodNull))
|
|
56
|
+
return nonEmptyOption ? unwrap(nonEmptyOption) : options[0]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (schema instanceof z.ZodEffects)
|
|
60
|
+
return unwrap(schema._def.schema)
|
|
61
|
+
|
|
62
|
+
return schema
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function notEmpty(key: string, schema: SchemaStructure): string | Record<string, SchemaStructure> {
|
|
66
|
+
return schema?.length ? {[key]: schema} : key
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function Schema<
|
|
70
|
+
T extends ZodTypeAny,
|
|
71
|
+
B extends object
|
|
72
|
+
>(
|
|
73
|
+
schema: T,
|
|
74
|
+
BaseClass?: new (...args: any[]) => B
|
|
75
|
+
) {
|
|
76
|
+
const Base = (BaseClass || class {})
|
|
77
|
+
|
|
78
|
+
return class extends Base {
|
|
79
|
+
static _schema = schema
|
|
80
|
+
static defaultSortKey?: string
|
|
81
|
+
|
|
82
|
+
#PK?: string
|
|
83
|
+
#SK?: string
|
|
84
|
+
|
|
85
|
+
constructor(data: z.infer<T>) {
|
|
86
|
+
super()
|
|
87
|
+
Object.assign(this, data)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get PK() { return this.#PK }
|
|
91
|
+
get SK() { return this.#SK }
|
|
92
|
+
|
|
93
|
+
static get schema() {
|
|
94
|
+
return extractZodKeys(this._schema)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static get defaultSK() {
|
|
98
|
+
return this.defaultSortKey
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
withKey(key: string, sk?: string) {
|
|
102
|
+
this.#PK = key
|
|
103
|
+
if (sk) this.#SK = sk
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Operator = '=' | '<>' | '<' | '<=' | '>' | '>=' | 'begins_with' | 'between' | 'in' | 'attribute_exists' | 'attribute_not_exists' | 'attribute_type' | 'contains' | 'size'
|
|
2
|
+
|
|
3
|
+
export type Condition = {
|
|
4
|
+
type: 'filter' | 'keyCondition',
|
|
5
|
+
field: string,
|
|
6
|
+
operator: Operator,
|
|
7
|
+
value: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ISchemaStructure = string | Record<string, ISchemaStructure[]>
|
|
11
|
+
export type SchemaStructure = ISchemaStructure[]
|
|
12
|
+
|
|
13
|
+
export type KeySchema = Record<'PK' | 'SK', string>
|
|
14
|
+
export type ModelMetadata = {
|
|
15
|
+
table: string,
|
|
16
|
+
keys?: KeySchema,
|
|
17
|
+
defaultSK?: string,
|
|
18
|
+
zip: boolean,
|
|
19
|
+
fields: SchemaStructure,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ModelOpts = string | {
|
|
23
|
+
table?: string,
|
|
24
|
+
partitionKey?: string,
|
|
25
|
+
sortKey?: string,
|
|
26
|
+
defaultSK?: string,
|
|
27
|
+
zip?: boolean,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Keys = string | [string] | [string, string]
|
|
31
|
+
export type Model<T extends object> = new (...args: any[]) => T
|
|
32
|
+
export type Filter<T> = (item: T) => boolean
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { default as QueryBuilder } from './query-builder'
|
package/src/model.ts
CHANGED
package/src/query-builder.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
Pipe,
|
|
19
19
|
} from './types'
|
|
20
20
|
|
|
21
|
-
export class QueryBuilder<
|
|
21
|
+
export default class QueryBuilder<
|
|
22
22
|
S,
|
|
23
23
|
T,
|
|
24
24
|
C extends keyof T = keyof T //,
|
|
@@ -42,23 +42,19 @@ export class QueryBuilder<
|
|
|
42
42
|
#joins: string[] = []
|
|
43
43
|
|
|
44
44
|
#pipe?: Pipe<S, T, C>
|
|
45
|
-
// #runFn?: RunFn<S, T, C>
|
|
46
45
|
|
|
47
46
|
constructor(
|
|
48
47
|
table: string,
|
|
49
48
|
schema?: DBSchema,
|
|
50
49
|
pipe?: Pipe<S, T, C>
|
|
51
|
-
// runFn?: RunFn<S, T, C>
|
|
52
50
|
) {
|
|
53
51
|
this.#table = table
|
|
54
52
|
this.#schema = schema
|
|
55
53
|
this.#pipe = pipe
|
|
56
|
-
// this.#runFn = runFn
|
|
57
54
|
this.#clauses = new ClauseBuilder<T>(table, schema)
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
async run() {
|
|
61
|
-
// if (!this.#runFn)
|
|
62
58
|
if (!this.#pipe?.run)
|
|
63
59
|
throw new Error(`No database connection.`)
|
|
64
60
|
|
package/src/types.ts
CHANGED