forj 0.0.1 → 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.
@@ -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
+ }