forj 0.0.2 → 0.0.4

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,13 +1,16 @@
1
1
  {
2
2
  "name": "forj",
3
3
  "description": "SQLite ORM and Query Builder whitout dependencies",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
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",
12
+ "./dynamodb": "./src/dynamodb/index.ts",
13
+ "./dynamodb/types": "./src/dynamodb/types.ts"
11
14
  },
12
15
  "-exports": {
13
16
  ".": {
@@ -25,12 +28,15 @@
25
28
  "-prepublishOnly": "bun run build"
26
29
  },
27
30
  "dependencies": {
28
- "@cloudflare/workers-types": "^4.20260113.0",
31
+ "@aws-sdk/client-dynamodb": "3.817.0",
32
+ "@aws-sdk/lib-dynamodb": "3.817.0",
29
33
  "pluralize": "^8.0",
34
+ "t0n": "^0.1",
30
35
  "zod": "^3.19.1"
31
36
  },
32
37
  "devDependencies": {
33
38
  "@types/pluralize": "^0.0.33",
39
+ "@cloudflare/workers-types": "^4.20260113.0",
34
40
  "bun-types": "^1.2.13",
35
41
  "terser": "^5.46.0",
36
42
  "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 '@cloudflare/workers-types'
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 { QueryBuilder } from '../query-builder'
10
- import { default as BaseModel } from '../model'
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 ModelBuilder<
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 Model<TBase, S> {
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 default ModelBuilder
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 in process.env))
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 process.env[this.$db] as D1Database
46
+ return Envir.get(this.$db) as D1Database
48
47
  }
49
48
 
50
49
  return this.$db
@@ -0,0 +1,7 @@
1
+ export type {
2
+ D1Database,
3
+ D1PreparedStatement,
4
+ D1Result, D1ExecResult, D1Meta,
5
+ } from '@cloudflare/workers-types'
6
+
7
+ export * from '../types'
@@ -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,4 @@
1
+ export { Dynamodb, DocumentClient, RawClient } from './client'
2
+ export { Model, Entity, Zip, PartitionKey, SortKey, Key, Keys } from './decorators'
3
+ export { Schema } from './schema'
4
+ export { Repository } from './repository'
@@ -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 * from './query-builder'
1
+ export { default as QueryBuilder } from './query-builder'
package/src/model.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import pluralize from 'pluralize'
2
- import { QueryBuilder } from './query-builder'
2
+ import QueryBuilder from './query-builder'
3
3
  import type {
4
4
  Operator, OrderDirection,
5
5
  WhereFn, WhereArgs,
@@ -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
@@ -1,5 +1,5 @@
1
1
  import z from 'zod'
2
- import { QueryBuilder } from './query-builder'
2
+ import QueryBuilder from './query-builder'
3
3
 
4
4
  export type text = string
5
5
  export type real = number