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.
- package/package.json +13 -4
- package/src/clause-builder.ts +216 -0
- package/src/d1/index.ts +4 -0
- package/src/d1/model.ts +78 -0
- 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 -0
- package/src/model.ts +258 -0
- package/src/query-builder.ts +371 -0
- package/src/types.ts +260 -0
- package/src/utils.ts +134 -0
- package/dist/chunk-NVO75XBO.js +0 -1
- package/dist/d1.d.ts +0 -113
- package/dist/d1.js +0 -1
- package/dist/index-CwrzXlna.d.ts +0 -163
- package/dist/index.ts.d.ts +0 -2
- package/dist/index.ts.js +0 -1
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
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
|
+
"files": ["src"],
|
|
7
8
|
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./d1": "./src/d1/index.ts",
|
|
11
|
+
"./d1/types": "./src/d1/types.ts"
|
|
12
|
+
},
|
|
13
|
+
"-exports": {
|
|
8
14
|
".": {
|
|
9
15
|
"import": "./dist/index.js",
|
|
10
16
|
"types": "./dist/index.d.ts"
|
|
@@ -17,15 +23,18 @@
|
|
|
17
23
|
"scripts": {
|
|
18
24
|
"bench": "tsx tests/benchmark/*.bench.ts",
|
|
19
25
|
"build": "tsup",
|
|
20
|
-
"prepublishOnly": "bun run build"
|
|
26
|
+
"-prepublishOnly": "bun run build"
|
|
21
27
|
},
|
|
22
28
|
"dependencies": {
|
|
23
|
-
"@
|
|
29
|
+
"@aws-sdk/client-dynamodb": "3.817.0",
|
|
30
|
+
"@aws-sdk/lib-dynamodb": "3.817.0",
|
|
24
31
|
"pluralize": "^8.0",
|
|
32
|
+
"t0n": "^0.1",
|
|
25
33
|
"zod": "^3.19.1"
|
|
26
34
|
},
|
|
27
35
|
"devDependencies": {
|
|
28
36
|
"@types/pluralize": "^0.0.33",
|
|
37
|
+
"@cloudflare/workers-types": "^4.20260113.0",
|
|
29
38
|
"bun-types": "^1.2.13",
|
|
30
39
|
"terser": "^5.46.0",
|
|
31
40
|
"tiny-glob": "^0.2",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { parseColumn, isJoinCompare, zSame, zType } from './utils'
|
|
2
|
+
import type {
|
|
3
|
+
IClauseBuilder,
|
|
4
|
+
ClauseOperator,
|
|
5
|
+
Value, Values,
|
|
6
|
+
WhereFn, WhereArgs,
|
|
7
|
+
DBSchema,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
export default class ClauseBuilder<
|
|
11
|
+
T,
|
|
12
|
+
C extends keyof T = keyof T
|
|
13
|
+
> implements IClauseBuilder<T> {
|
|
14
|
+
#table: string
|
|
15
|
+
#schema?: DBSchema
|
|
16
|
+
#clauses: string[] = []
|
|
17
|
+
#args: Values = []
|
|
18
|
+
|
|
19
|
+
get clauses() {
|
|
20
|
+
return this.#clauses
|
|
21
|
+
}
|
|
22
|
+
set clauses(clauses: string[]) {
|
|
23
|
+
this.#clauses.push(...clauses)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get args() {
|
|
27
|
+
return this.#args
|
|
28
|
+
}
|
|
29
|
+
set args(args: Values) {
|
|
30
|
+
this.#args.push(...args)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get length() {
|
|
34
|
+
return this.#clauses.length
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(table: string, schema?: DBSchema) {
|
|
38
|
+
this.#table = table
|
|
39
|
+
this.#schema = schema
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#nested(fn: WhereFn<T, C>, operator: ClauseOperator = 'AND') {
|
|
43
|
+
const nested = new ClauseBuilder<T, C>(this.#table, this.#schema)
|
|
44
|
+
fn(nested)
|
|
45
|
+
|
|
46
|
+
if (nested.length) {
|
|
47
|
+
this.#clauses.push(`${this.length ? operator +' ' : ''}(${nested.clauses.join(' ')})`)
|
|
48
|
+
this.#args.push(...nested.args)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#clause(sql: string, values: Values = [], bool: ClauseOperator = 'AND') {
|
|
55
|
+
if (this.length) sql = bool +' '+ sql
|
|
56
|
+
this.#clauses.push(sql)
|
|
57
|
+
|
|
58
|
+
if (values?.length) // TODO: https://developers.cloudflare.com/d1/worker-api/#type-conversion
|
|
59
|
+
this.#args.push(...values)
|
|
60
|
+
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#where(
|
|
65
|
+
logical: ClauseOperator,
|
|
66
|
+
...args: WhereArgs<T>
|
|
67
|
+
) {
|
|
68
|
+
if (typeof args[0] == 'function')
|
|
69
|
+
return this.#nested(args[0], logical)
|
|
70
|
+
|
|
71
|
+
const length = args.length
|
|
72
|
+
let [column, operator, value] = args
|
|
73
|
+
|
|
74
|
+
if (length == 2) { // @ts-ignore
|
|
75
|
+
value = operator
|
|
76
|
+
operator = '='
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
column = parseColumn(String(column), this.#table)
|
|
81
|
+
|
|
82
|
+
if (this.#schema && !zSame(column, value, this.#schema)) {
|
|
83
|
+
throw new Error(`Table column '${String(column)}' of type '${zType(column, this.#schema)}' is not assignable as type of '${typeof value}'.`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return isJoinCompare(value, this.#schema) // @ts-ignore
|
|
87
|
+
? this.#clause(`${column} ${operator} ${value}`, [], logical) // @ts-ignore
|
|
88
|
+
: this.#clause(`${column} ${operator} ?`, [value], logical)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
where(...args: WhereArgs<T>) {
|
|
92
|
+
return this.#where('AND', ...args)
|
|
93
|
+
}
|
|
94
|
+
on(...args: WhereArgs<T>) {
|
|
95
|
+
return this.where(...args)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
orWhere(...args: WhereArgs<T>) {
|
|
99
|
+
return this.#where('OR', ...args)
|
|
100
|
+
}
|
|
101
|
+
orOn(...args: WhereArgs<T>) {
|
|
102
|
+
return this.orWhere(...args)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#in(
|
|
106
|
+
column: string,
|
|
107
|
+
values: Values,
|
|
108
|
+
operator: 'IN' | 'NOT IN',
|
|
109
|
+
logical: ClauseOperator = 'AND'
|
|
110
|
+
) {
|
|
111
|
+
if (!values?.length) return this
|
|
112
|
+
return this.#clause(parseColumn(column, this.#table) + ` ${operator} (${values.map(() => '?').join(', ')})`, values, logical)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
whereIn(column: C, values: T[C][]) { // @ts-ignore
|
|
116
|
+
return this.#in(column, values, 'IN')
|
|
117
|
+
}
|
|
118
|
+
in(column: C, values: T[C][]) {
|
|
119
|
+
return this.whereIn(column, values)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
whereNotIn(column: C, values: T[C][]) { // @ts-ignore
|
|
123
|
+
return this.#in(column, values, 'NOT IN')
|
|
124
|
+
}
|
|
125
|
+
notIn(column: C, values: T[C][]) {
|
|
126
|
+
return this.whereNotIn(column, values)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
orWhereIn(column: C, values: T[C][]) { // @ts-ignore
|
|
130
|
+
return this.#in(column, values, 'IN', 'OR')
|
|
131
|
+
}
|
|
132
|
+
orIn(column: C, values: T[C][]) {
|
|
133
|
+
return this.orWhereIn(column, values)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
orWhereNotIn(column: C, values: T[C][]) { // @ts-ignore
|
|
137
|
+
return this.#in(column, values, 'NOT IN', 'OR')
|
|
138
|
+
}
|
|
139
|
+
orNotIn(column: C, values: T[C][]) {
|
|
140
|
+
return this.orWhereNotIn(column, values)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#between(
|
|
144
|
+
column: string,
|
|
145
|
+
one: Value,
|
|
146
|
+
two: Value,
|
|
147
|
+
operator: 'BETWEEN' | 'NOT BETWEEN',
|
|
148
|
+
logical: ClauseOperator = 'AND'
|
|
149
|
+
) {
|
|
150
|
+
return this.#clause(parseColumn(column, this.#table) + ` ${operator} ? AND ?`, [one, two], logical)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
whereBetween(column: C, one: T[C], two: T[C]) { // @ts-ignore
|
|
154
|
+
return this.#between(column, one, two, 'BETWEEN')
|
|
155
|
+
}
|
|
156
|
+
between(column: C, one: T[C], two: T[C]) {
|
|
157
|
+
return this.whereBetween(column, one, two)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
orWhereBetween(column: C, one: T[C], two: T[C]) { // @ts-ignore
|
|
161
|
+
return this.#between(column, one, two, 'BETWEEN', 'OR')
|
|
162
|
+
}
|
|
163
|
+
orBetween(column: C, one: T[C], two: T[C]) {
|
|
164
|
+
return this.orWhereBetween(column, one, two)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
whereNotBetween(column: C, one: T[C], two: T[C]) { // @ts-ignore
|
|
168
|
+
return this.#between(column, one, two, 'NOT BETWEEN')
|
|
169
|
+
}
|
|
170
|
+
notBetween(column: C, one: T[C], two: T[C]) {
|
|
171
|
+
return this.whereNotBetween(column, one, two)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
orWhereNotBetween(column: C, one: T[C], two: T[C]) { // @ts-ignore
|
|
175
|
+
return this.#between(column, one, two, 'NOT BETWEEN', 'OR')
|
|
176
|
+
}
|
|
177
|
+
orNotBetween(column: C, one: T[C], two: T[C]) {
|
|
178
|
+
return this.orWhereNotBetween(column, one, two)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#null(
|
|
182
|
+
column: string,
|
|
183
|
+
operator: 'IS' | 'IS NOT' = 'IS',
|
|
184
|
+
logical: ClauseOperator = 'AND'
|
|
185
|
+
) {
|
|
186
|
+
return this.#clause(parseColumn(column, this.#table) +` ${operator} NULL`, [], logical)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
whereNull(column: C) {
|
|
190
|
+
return this.#null(column as string)
|
|
191
|
+
}
|
|
192
|
+
onNull(column: C) {
|
|
193
|
+
return this.whereNull(column)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
orWhereNull(column: C) {
|
|
197
|
+
return this.#null(column as string, 'IS', 'OR')
|
|
198
|
+
}
|
|
199
|
+
orOnNull(column: C) {
|
|
200
|
+
return this.orWhereNull(column)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
whereNotNull(column: C) {
|
|
204
|
+
return this.#null(column as string, 'IS NOT')
|
|
205
|
+
}
|
|
206
|
+
onNotNull(column: C) {
|
|
207
|
+
return this.whereNotNull(column)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
orWhereNotNull(column: C) {
|
|
211
|
+
return this.#null(column as string, 'IS NOT', 'OR')
|
|
212
|
+
}
|
|
213
|
+
orNotNull(column: C) {
|
|
214
|
+
return this.orWhereNotNull(column)
|
|
215
|
+
}
|
|
216
|
+
}
|
package/src/d1/index.ts
ADDED
package/src/d1/model.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
D1Database,
|
|
3
|
+
// D1PreparedStatement,
|
|
4
|
+
// D1Result, D1ExecResult, D1Meta,
|
|
5
|
+
} from './types'
|
|
6
|
+
|
|
7
|
+
import z from 'zod'
|
|
8
|
+
import { Envir } from 't0n'
|
|
9
|
+
|
|
10
|
+
import QueryBuilder from '../query-builder'
|
|
11
|
+
import BModel from '../model'
|
|
12
|
+
import type {
|
|
13
|
+
DBSchema, SchemaKeys,
|
|
14
|
+
Item,
|
|
15
|
+
Pipe, Result, RunFn,
|
|
16
|
+
} from '../types'
|
|
17
|
+
|
|
18
|
+
export function Model<
|
|
19
|
+
TSchema extends DBSchema,
|
|
20
|
+
TBase extends SchemaKeys<TSchema>
|
|
21
|
+
>(schema: TSchema, base: TBase) {
|
|
22
|
+
type S = z.infer<typeof schema>
|
|
23
|
+
return class extends BaseModel<TBase, S> {
|
|
24
|
+
static $table = String(base)
|
|
25
|
+
static $schema = schema
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export abstract class BaseModel<TB extends keyof DB, DB> extends BModel<TB, DB> {
|
|
30
|
+
static $db: string | D1Database = 'DB'
|
|
31
|
+
|
|
32
|
+
static pipe<S, T>(): Pipe<S, T> {
|
|
33
|
+
const db = this.DB()
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
run: this.run<S, T>(db)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static DB() {
|
|
41
|
+
if (typeof this.$db == 'string') { // TODO: improv compatibility without nodejs_compat
|
|
42
|
+
if (!Envir.has(this.$db))
|
|
43
|
+
throw new Error(`Database '${this.$db}' instance not provided.`)
|
|
44
|
+
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
return Envir.get(this.$db) as D1Database
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.$db
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static run<S, T>(db: D1Database): RunFn<S, T> {
|
|
53
|
+
return async <S, T, C extends keyof T = keyof T>(
|
|
54
|
+
qb: QueryBuilder<S, T, C>
|
|
55
|
+
): Promise<Result<T, C>> => {
|
|
56
|
+
let stmt = db.prepare(qb.query)
|
|
57
|
+
|
|
58
|
+
if (qb.args?.length)
|
|
59
|
+
stmt = stmt.bind(...qb.args)
|
|
60
|
+
|
|
61
|
+
const resp = await stmt.run<Item<T, C>>()
|
|
62
|
+
|
|
63
|
+
const meta = resp.meta as any
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
changes: meta?.changes,
|
|
67
|
+
duration: meta?.duration,
|
|
68
|
+
lastId: meta?.last_row_id,
|
|
69
|
+
// served_by: meta?.served_by,
|
|
70
|
+
rowsRead: meta?.rows_read,
|
|
71
|
+
rowsWritten: meta?.rows_written,
|
|
72
|
+
// meta: resp.meta,
|
|
73
|
+
success: resp.success,
|
|
74
|
+
results: resp.results,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
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
|
+
}
|