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 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.1",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
- "files": ["dist"],
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
- "@cloudflare/workers-types": "^4.20260113.0",
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
+ }
@@ -0,0 +1,4 @@
1
+
2
+ // export * from './migration'
3
+ // export * from './seeder'
4
+ export * from './model'
@@ -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
+ }
@@ -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
+ }