w-orm-reladb 1.1.5 → 1.1.7

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,393 @@
1
+ import assert from 'assert'
2
+ import Sequelize from 'sequelize'
3
+ import cvFind, { getColKind, normPk } from '../src/cvFind.mjs'
4
+
5
+
6
+ //cvFind為select與delAll共用之條件轉譯純函數, 本檔不連線資料庫
7
+ //斷言分兩類: 拒收(reject之訊息)與方言專屬之SQL片段(ESCAPE、N前綴、LOWER內之實際欄名);
8
+ //命中集合之語意由test/api-suite.mjs之find段於sqlite與mssql實際執行斷言, 本檔不以SQL之括號結構作斷言
9
+ //對應CLAUDE_rulebook.md之R1–R6與R8(normPk)
10
+
11
+
12
+ let Op = Sequelize.Op
13
+
14
+ let defs = {
15
+ 'id': { type: Sequelize.STRING, primaryKey: true },
16
+ 's': Sequelize.STRING,
17
+ 't': Sequelize.TEXT,
18
+ 'i': Sequelize.INTEGER,
19
+ 'f': Sequelize.FLOAT,
20
+ 'p': Sequelize.DOUBLE,
21
+ 'dec': Sequelize.DECIMAL(10, 2),
22
+ 'b': Sequelize.BOOLEAN,
23
+ 'd': Sequelize.DATE,
24
+ 'do': Sequelize.DATEONLY,
25
+ 'big': Sequelize.BIGINT,
26
+ 'length': Sequelize.INTEGER,
27
+ 'title': { type: Sequelize.TEXT, field: 'physical_title' },
28
+ 'p$x': Sequelize.STRING,
29
+ }
30
+
31
+ //離線之sequelize實例, 建構時不連線
32
+ let mk = (dialect) => {
33
+ let sq = dialect === 'sqlite'
34
+ ? new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
35
+ : new Sequelize('worm', 'u', 'p', { dialect: 'mssql', host: 'localhost', port: 1, logging: false })
36
+ let md = sq.define('tcond', defs, { tableName: 'tcond', timestamps: false })
37
+ let qg = sq.getQueryInterface().queryGenerator
38
+ let ctx = { escape: (v) => sq.escape(v), attrs: md.rawAttributes, dialect }
39
+ let where = (find, ex = {}) => {
40
+ let sql = qg.selectQuery('tcond', { where: cvFind(find, { ...ctx, ...ex }) }, md)
41
+ let k = sql.indexOf(' WHERE ')
42
+ return k >= 0 ? sql.slice(k + 7).replace(/;$/, '') : ''
43
+ }
44
+ return { ctx, where }
45
+ }
46
+ let LT = mk('sqlite')
47
+ let MS = mk('mssql')
48
+
49
+ let throwsMsg = (fn, msg) => {
50
+ let e = null
51
+ try {
52
+ fn()
53
+ }
54
+ catch (err) {
55
+ e = err
56
+ }
57
+ assert.ok(e !== null, `未拋錯, 應含[${msg}]`)
58
+ assert.ok(String(e.message).indexOf(msg) >= 0, `訊息不含[${msg}]: ${e.message}`)
59
+ }
60
+
61
+
62
+ describe('cvFind', function() {
63
+
64
+ it('R1 非物件與陣列之find回空物件(現況保留), 非一般物件拒收', function() {
65
+ for (let v of [undefined, null, [], 'a', 1, true, [{ id: 'a' }]]) {
66
+ assert.deepStrictEqual(cvFind(v, LT.ctx), {})
67
+ }
68
+ assert.deepStrictEqual(cvFind({}, LT.ctx), {})
69
+ class Q {
70
+ constructor() {
71
+ this.s = 'a'
72
+ }
73
+ }
74
+ for (let v of [Object.create({ s: 'a' }), new Q(), new Date(), new Map([['s', 'a']]), /a/]) {
75
+ throwsMsg(() => cvFind(v, LT.ctx), 'find must be a plain object')
76
+ }
77
+ assert.ok(LT.where(Object.assign(Object.create(null), { s: 'a' })).indexOf('`s` = \'a\'') >= 0)
78
+ })
79
+
80
+ it('R2 欄名length不得使其他條件失效, 欄名含$視為欄名', function() {
81
+ let w = LT.where({ s: 'x', length: 0 })
82
+ assert.ok(w.indexOf('`s` = \'x\'') >= 0 && w.indexOf('`length` = 0') >= 0, w)
83
+ assert.ok(LT.where({ 'p$x': 'q' }).indexOf('`p$x` = \'q\'') >= 0)
84
+ })
85
+
86
+ it('R2 Symbol須為sequelize之運算子, 欄位層之邏輯Symbol拒收', function() {
87
+ throwsMsg(() => cvFind({ i: { [Op.gt]: { x: 1 } } }, LT.ctx), 'value must be')
88
+ throwsMsg(() => cvFind({ [Op.like]: 'x' }, LT.ctx), 'unsupported symbol operator')
89
+ throwsMsg(() => cvFind({ [Symbol('or')]: [{ s: 'a' }] }, LT.ctx), 'unsupported symbol operator')
90
+ throwsMsg(() => cvFind({ i: { [Symbol('gt')]: 1 } }, LT.ctx), 'unsupported symbol operator')
91
+ throwsMsg(() => cvFind({ i: { [Op.or]: [{ [Op.eq]: 1 }], [Op.gt]: 10 } }, LT.ctx), 'unsupported symbol operator')
92
+ throwsMsg(() => cvFind({ i: { [Op.or]: [] } }, LT.ctx), 'unsupported symbol operator')
93
+ throwsMsg(() => cvFind({ i: { [Op.and]: 0 } }, LT.ctx), 'unsupported symbol operator')
94
+ throwsMsg(() => cvFind({ s: 'a', [Symbol.toStringTag]: 'Q' }, LT.ctx), 'unsupported symbol operator')
95
+ //Symbol與對應之$運算子產生相同之條件
96
+ assert.strictEqual(LT.where({ i: { [Op.not]: { [Op.gt]: 2 } } }), LT.where({ i: { $not: { $gt: 2 } } }))
97
+ assert.strictEqual(LT.where({ i: { [Op.in]: [1, 2] } }), LT.where({ i: { $in: [1, 2] } }))
98
+ })
99
+
100
+ it('R2 同義之鍵各自成一子式, 非列舉之鍵亦納入', function() {
101
+ let w = LT.where({ i: { $gt: 5, [Op.gt]: 1 } })
102
+ assert.ok(w.indexOf('`i` > 5') >= 0 && w.indexOf('`i` > 1') >= 0, w)
103
+ let w2 = LT.where({ $and: [{ s: 'a' }], [Op.and]: [{ s: 'b' }] })
104
+ assert.ok(w2.indexOf('`s` = \'a\'') >= 0 && w2.indexOf('`s` = \'b\'') >= 0, w2)
105
+ let f = {}
106
+ Object.defineProperty(f, 's', { value: 'a', enumerable: false })
107
+ assert.ok(LT.where(f).indexOf('`s` = \'a\'') >= 0)
108
+ })
109
+
110
+ it('R2 欄名__proto__與空字串拒收', function() {
111
+ throwsMsg(() => cvFind(JSON.parse('{"__proto__":1}'), LT.ctx), 'invalid column name')
112
+ throwsMsg(() => cvFind({ '': 1 }, LT.ctx), 'invalid column name')
113
+ })
114
+
115
+ it('R3 不支援之運算子一律拒收', function() {
116
+ let ops = ['$foo', '$like', '$startsWith', '$between', '$col', '$iLike', '$regexp', '$type', '$mod', '$size', '$all', '$elemMatch']
117
+ for (let op of ops) {
118
+ throwsMsg(() => cvFind({ i: { [op]: 1 } }, LT.ctx), `unsupported operator [${op}]`)
119
+ }
120
+ for (let op of ['$not', '$where', '$expr', '$foo', '$comment']) {
121
+ throwsMsg(() => cvFind({ [op]: { i: 1 } }, LT.ctx), `unsupported operator [${op}]`)
122
+ }
123
+ })
124
+
125
+ it('R3 邏輯運算子須為非空之物件陣列, 陣列不得有洞', function() {
126
+ for (let op of ['$and', '$or', '$nor']) {
127
+ throwsMsg(() => cvFind({ [op]: [] }, LT.ctx), 'nonempty array')
128
+ throwsMsg(() => cvFind({ [op]: { s: 1 } }, LT.ctx), 'requires an array')
129
+ throwsMsg(() => cvFind({ [op]: ['x'] }, LT.ctx), 'condition must be an object')
130
+ throwsMsg(() => cvFind({ [op]: new Array(2) }, LT.ctx), 'holes or undefined')
131
+ }
132
+ throwsMsg(() => cvFind({ [Op.and]: [] }, LT.ctx), 'nonempty array')
133
+ throwsMsg(() => cvFind({ i: { $in: [1, undefined] } }, LT.ctx), 'holes or undefined')
134
+ throwsMsg(() => cvFind({ i: { $exists: 'yes' } }, LT.ctx), '$exists must be a boolean, 0 or 1')
135
+ })
136
+
137
+ it('R4 非純量值不得交sequelize之型別stringify', function() {
138
+ let bads = [
139
+ [{ d: { $lt: { x: 1 } } }, 'value must be'],
140
+ [{ f: { $gt: [1] } }, 'value must be'],
141
+ [{ i: { $in: [{}] } }, 'value must be'],
142
+ [{ i: NaN }, 'number must be finite'],
143
+ [{ i: Infinity }, 'number must be finite'],
144
+ [{ d: new Date('x') }, 'invalid Date'],
145
+ [{ i: 10n }, 'value must be'],
146
+ [{ s: () => 1 }, 'value must be'],
147
+ [{ s: Symbol('x') }, 'value must be'],
148
+ [{ s: new Map() }, 'value must be'],
149
+ [{ s: undefined }, 'value is undefined'],
150
+ [{ i: {} }, 'empty operator object'],
151
+ [{ s: { a: 1 } }, 'nested object is not supported'],
152
+ [{ i: { $gt: null } }, 'null is not allowed'],
153
+ [{ i: { $in: 1 } }, 'must be an array'],
154
+ [{ i: { $not: [1] } }, '$not does not accept an array'],
155
+ [{ i: { $not: {} } }, 'empty operator object'],
156
+ ]
157
+ for (let [f, msg] of bads) {
158
+ throwsMsg(() => cvFind(f, LT.ctx), msg)
159
+ }
160
+ })
161
+
162
+ it('R4 純量須符合欄位型別', function() {
163
+ let bads = [
164
+ [{ i: { $lt: 'abc' } }, '[int]'],
165
+ [{ i: { $lt: '0x10' } }, '[int]'],
166
+ [{ i: { $lt: '0b1' } }, '[int]'],
167
+ [{ i: ' 5' }, '[int]'],
168
+ [{ i: '2.5' }, '[int]'],
169
+ [{ i: '1e3' }, '[int]'],
170
+ [{ i: '5.' }, '[int]'],
171
+ [{ dec: '1e3' }, '[num]'],
172
+ [{ f: '5.' }, '[num]'],
173
+ [{ dec: '1.254' }, '[num] with scale [2]'],
174
+ [{ d: { $lt: 1e16 } }, '[date]'],
175
+ [{ d: -8.64e15 - 1 }, '[date]'],
176
+ [{ i: Number('9007199254740993') }, '[int]'],
177
+ [{ big: Number('9007199254740993') }, '[int]'],
178
+ [{ f: { $lt: 'abc' } }, '[num]'],
179
+ [{ p: { $lt: 'abc' } }, '[num]'],
180
+ [{ dec: { $lt: true } }, '[num]'],
181
+ [{ d: { $lt: 'garbage' } }, '[date]'],
182
+ [{ d: { $lt: '2020-01-01' } }, '[date]'],
183
+ [{ d: { $lt: '2026-02-30T00:00:00Z' } }, '[date]'],
184
+ [{ d: { $lt: true } }, '[date]'],
185
+ [{ do: new Date('2020-01-01T00:00:00Z') }, '[dateonly]'],
186
+ [{ do: '2026-02-30' }, '[dateonly]'],
187
+ [{ s: 1 }, '[str]'],
188
+ [{ b: 'abc' }, '[bool]'],
189
+ ]
190
+ for (let [f, msg] of bads) {
191
+ throwsMsg(() => cvFind(f, LT.ctx), `does not match column type ${msg}`)
192
+ }
193
+ //Buffer只用於未分類型別之欄
194
+ throwsMsg(() => cvFind({ s: Buffer.from('a') }, LT.ctx), 'value must be')
195
+ //相符者放行
196
+ for (let f of [{ i: '2' }, { i: '-25' }, { f: '-2.5' }, { dec: '.5' }, { dec: '1.25' }, { dec: 1.254 }, { d: 8.64e15 }, { big: '9007199254740993' }, { p: 2.5 }, { d: { $lt: '2020-01-01T00:00:00Z' } }, { d: { $lt: '2020-01-01 08:00:00+08:00' } }, { d: { $lt: 1577836800000 } }, { do: '2020-01-01' }, { b: 1 }, { nocol: 1 }]) {
197
+ assert.doesNotThrow(() => cvFind(f, LT.ctx), JSON.stringify(f))
198
+ }
199
+ })
200
+
201
+ it('R4 巢狀深度上限與循環參照', function() {
202
+ let o = {}
203
+ o.$and = [o]
204
+ throwsMsg(() => cvFind(o, LT.ctx), 'too deep')
205
+ })
206
+
207
+ it('R6 $regex跳脫LIKE萬用字元並帶ESCAPE, mssql為N前綴', function() {
208
+ assert.ok(LT.where({ t: { $regex: '_' } }).indexOf('LIKE \'%!_%\' ESCAPE \'!\'') >= 0)
209
+ assert.ok(LT.where({ t: { $regex: '%' } }).indexOf('LIKE \'%!%%\' ESCAPE \'!\'') >= 0)
210
+ assert.ok(LT.where({ t: { $regex: '!' } }).indexOf('LIKE \'%!!%\' ESCAPE \'!\'') >= 0)
211
+ assert.ok(LT.where({ t: { $regex: '\\[a-c\\]' } }).indexOf('LIKE \'%![a-c]%\' ESCAPE \'!\'') >= 0)
212
+ assert.ok(LT.where({ t: { $regex: 'a\\.c' } }).indexOf('LIKE \'%a.c%\'') >= 0)
213
+ assert.ok(MS.where({ t: { $regex: '_' } }).indexOf('LIKE N\'%!_%\' ESCAPE \'!\'') >= 0)
214
+ assert.ok(MS.where({ t: { $regex: '中' } }).indexOf('LIKE N\'%中%\'') >= 0)
215
+ })
216
+
217
+ it('R6 $regex只收字面子字串, 旗標只收i', function() {
218
+ for (let p of ['^pe', 'a.c', 'a*', '[a]', 'a|b', '(a)', 'a{2}', 'a?', 'a+', 'a$', '\\d', 'a/b', 'a\\']) {
219
+ throwsMsg(() => cvFind({ t: { $regex: p } }, LT.ctx), '$regex supports literal substrings only')
220
+ }
221
+ throwsMsg(() => cvFind({ t: { $regex: 'a' + String.fromCharCode(0) + 'x' } }, LT.ctx), 'NUL')
222
+ throwsMsg(() => cvFind({ t: { $regex: 'a', $options: 'm' } }, LT.ctx), 'supports [i] only')
223
+ throwsMsg(() => cvFind({ t: { $regex: /a/m } }, LT.ctx), 'supports [i] only')
224
+ throwsMsg(() => cvFind({ t: { $regex: /a/, $options: 'i' } }, LT.ctx), '$options can not be used with a RegExp')
225
+ throwsMsg(() => cvFind({ t: { $options: 'i' } }, LT.ctx), '$options requires $regex')
226
+ throwsMsg(() => cvFind({ t: { $regex: 1 } }, LT.ctx), '$regex must be a string or RegExp')
227
+ for (let c of ['i', 'f', 'p', 'dec', 'b', 'd']) {
228
+ throwsMsg(() => cvFind({ [c]: { $regex: '1' } }, LT.ctx), '$regex requires a string column')
229
+ }
230
+ //同義寫法產生相同之條件
231
+ let wi = LT.where({ t: { $regex: 'PeT', $options: 'i' } })
232
+ assert.ok(wi.indexOf('LOWER(`t`) LIKE LOWER(\'%PeT%\') ESCAPE \'!\'') >= 0, wi)
233
+ assert.strictEqual(LT.where({ t: { $regex: 'PeT', $options: '$i' } }), wi)
234
+ assert.strictEqual(LT.where({ t: { $regex: /PeT/i } }), wi)
235
+ assert.strictEqual(LT.where({ t: /PeT/i }), wi)
236
+ assert.strictEqual(LT.where({ t: { $regex: new RegExp('') } }), LT.where({ t: { $regex: '' } }))
237
+ })
238
+
239
+ it('R6 給i時LOWER內為實際欄名; sqlite下含非ASCII大小寫字母者拒收', function() {
240
+ assert.ok(LT.where({ title: { $regex: 'a', $options: 'i' } }).indexOf('LOWER(`physical_title`)') >= 0)
241
+ throwsMsg(() => cvFind({ t: { $regex: 'é', $options: 'i' } }, LT.ctx), 'ASCII letters only')
242
+ assert.doesNotThrow(() => cvFind({ t: { $regex: '中', $options: 'i' } }, LT.ctx))
243
+ assert.ok(MS.where({ t: { $regex: 'é', $options: 'i' } }).indexOf('LOWER([t]) LIKE LOWER(N\'%é%\')') >= 0)
244
+ })
245
+
246
+ it('getColKind 依sequelize型別類別分類', function() {
247
+ let a = LT.ctx.attrs
248
+ let ks = ['id', 's', 't', 'i', 'f', 'p', 'dec', 'b', 'd', 'do', 'big', 'length', 'title', 'nocol']
249
+ assert.deepStrictEqual(ks.map((k) => getColKind(a, k)), ['str', 'str', 'str', 'int', 'num', 'num', 'num', 'bool', 'date', 'dateonly', 'int', 'int', 'str', 'any'])
250
+ let sq = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
251
+ let md = sq.define('tk', {
252
+ 'a': Sequelize.REAL,
253
+ 'b': Sequelize.SMALLINT,
254
+ 'c': Sequelize.CHAR(4),
255
+ 'd': Sequelize.UUID,
256
+ 'e': Sequelize.ENUM('x', 'y'),
257
+ 'f': Sequelize.TINYINT,
258
+ }, { tableName: 'tk', timestamps: false })
259
+ assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e', 'f'].map((k) => getColKind(md.rawAttributes, k)), ['num', 'int', 'str', 'str', 'any', 'int'])
260
+ })
261
+
262
+ it('R8 主鍵欄之條件值依normPk正規化, 非主鍵欄維持R4', function() {
263
+ //字串類主鍵欄: 有限數字即String(v), 含$in元素、範圍運算子、$not之內與sequelize原生Symbol
264
+ let pairs = [
265
+ [{ id: 123 }, { id: '123' }],
266
+ [{ id: { $in: [123, '45'] } }, { id: { $in: ['123', '45'] } }],
267
+ [{ id: [1.5] }, { id: ['1.5'] }],
268
+ [{ id: { $gt: 2, $lte: 9 } }, { id: { $gt: '2', $lte: '9' } }],
269
+ [{ id: { $not: { $eq: 7 } } }, { id: { $not: { $eq: '7' } } }],
270
+ [{ id: { [Op.in]: [7] } }, { id: { [Op.in]: ['7'] } }],
271
+ [{ id: { [Op.between]: [1, 2] } }, { id: { [Op.between]: ['1', '2'] } }],
272
+ ]
273
+ for (let [a, b] of pairs) {
274
+ assert.strictEqual(LT.where(a, { pk: 'id' }), LT.where(b, { pk: 'id' }))
275
+ assert.strictEqual(MS.where(a, { pk: 'id' }), MS.where(b, { pk: 'id' }))
276
+ }
277
+ assert.ok(LT.where({ id: 123 }, { pk: 'id' }).indexOf('`id` = \'123\'') >= 0)
278
+ assert.ok(MS.where({ id: 123 }, { pk: 'id' }).indexOf('[id] = N\'123\'') >= 0)
279
+ //null依R5, $regex不受影響
280
+ assert.ok(LT.where({ id: null }, { pk: 'id' }).indexOf('`id` IS NULL') >= 0)
281
+ assert.ok(LT.where({ id: { $regex: '12' } }, { pk: 'id' }).indexOf('LIKE \'%12%\'') >= 0)
282
+ //整數類主鍵欄: 標準整數寫法字串即數值, 超出安全整數者保留字串
283
+ assert.strictEqual(LT.where({ i: '12' }, { pk: 'i' }), LT.where({ i: 12 }, { pk: 'i' }))
284
+ assert.strictEqual(LT.where({ i: { $in: ['-3', 5] } }, { pk: 'i' }), LT.where({ i: { $in: [-3, 5] } }, { pk: 'i' }))
285
+ assert.ok(LT.where({ big: '9007199254740993' }, { pk: 'big' }).indexOf('\'9007199254740993\'') >= 0)
286
+ //其他數值類主鍵欄: 標準數字字串即數值
287
+ assert.strictEqual(LT.where({ dec: '1.25' }, { pk: 'dec' }), LT.where({ dec: 1.25 }, { pk: 'dec' }))
288
+ assert.strictEqual(LT.where({ f: '-2.5' }, { pk: 'f' }), LT.where({ f: -2.5 }, { pk: 'f' }))
289
+ //無效與型別不符拒收, 訊息同R4
290
+ let bads = [
291
+ [{ id: true }, 'id', '[str]'],
292
+ [{ id: '' }, 'id', '[str]'],
293
+ [{ id: { $ne: '' } }, 'id', '[str]'],
294
+ [{ id: { $in: [1, false] } }, 'id', '[str]'],
295
+ [{ i: '012' }, 'i', '[int]'],
296
+ [{ i: '+12' }, 'i', '[int]'],
297
+ [{ i: 12.4 }, 'i', '[int]'],
298
+ [{ i: { $gt: '1e1' } }, 'i', '[int]'],
299
+ [{ i: true }, 'i', '[int]'],
300
+ [{ big: Number('9007199254740993') }, 'big', '[int]'],
301
+ [{ dec: '1.50' }, 'dec', '[num]'],
302
+ [{ dec: '.5' }, 'dec', '[num]'],
303
+ [{ dec: 1.254 }, 'dec', '[num] with scale [2]'],
304
+ [{ dec: { $in: ['1.254'] } }, 'dec', '[num] with scale [2]'],
305
+ ]
306
+ for (let [f, pk, msg] of bads) {
307
+ throwsMsg(() => cvFind(f, { ...LT.ctx, pk }), `does not match column type ${msg}`)
308
+ }
309
+ //非主鍵欄維持R4: 字串欄收數值拒收、整數欄收'012'放行、DECIMAL欄之字串檢scale
310
+ throwsMsg(() => cvFind({ s: 1 }, { ...LT.ctx, pk: 'id' }), 'does not match column type [str]')
311
+ assert.doesNotThrow(() => cvFind({ i: '012' }, { ...LT.ctx, pk: 'id' }))
312
+ throwsMsg(() => cvFind({ dec: '1.254' }, { ...LT.ctx, pk: 'id' }), 'with scale [2]')
313
+ })
314
+
315
+ })
316
+
317
+
318
+ describe('normPk', function() {
319
+
320
+ //rulebook R8: 依主鍵欄型別正規化主鍵值, 與w-orm-mdb之normPk同一規則(主控複審run03第二節之表)
321
+
322
+ let ok = (v, kind, exp, scale) => {
323
+ assert.deepStrictEqual(normPk(v, kind, scale), { kind: 'ok', v: exp }, `${String(v)} @ ${kind} ${scale}`)
324
+ }
325
+ let no = (v, kind, r, scale) => {
326
+ assert.deepStrictEqual(normPk(v, kind, scale), { kind: r }, `${String(v)} @ ${kind} ${scale}`)
327
+ }
328
+
329
+ it('R8 字串類: 非空字串原樣, 有限數字為String(v)', function() {
330
+ ok('a', 'str', 'a')
331
+ ok(' a ', 'str', ' a ')
332
+ ok('123', 'str', '123')
333
+ ok(123, 'str', '123')
334
+ ok(1.5, 'str', '1.5')
335
+ ok(0, 'str', '0')
336
+ ok(-0, 'str', '0')
337
+ //包裝物件(同new String、new Number)先取原始值
338
+ ok(Object('a'), 'str', 'a')
339
+ ok(Object(5), 'str', '5')
340
+ })
341
+
342
+ it('R8 整數類: 安全整數原樣, 標準整數寫法字串為Number(s), 其餘為型別不符', function() {
343
+ ok(12, 'int', 12)
344
+ ok(-3, 'int', -3)
345
+ ok(0, 'int', 0)
346
+ ok(Number.MAX_SAFE_INTEGER, 'int', Number.MAX_SAFE_INTEGER)
347
+ ok('12', 'int', 12)
348
+ ok('-12', 'int', -12)
349
+ ok('0', 'int', 0)
350
+ //表外細化: 超出安全整數之標準整數寫法字串保留為字串(精確值, 供BIGINT)
351
+ ok('9007199254740993', 'int', '9007199254740993')
352
+ for (let v of [12.4, Number('9007199254740993'), 2 ** 53, 1e21, '012', '+12', '12.0', '1e1', ' 12', '12 ', '0x1', 'abc', '-0']) {
353
+ no(v, 'int', 'mismatch')
354
+ }
355
+ })
356
+
357
+ it('R8 其他數值類: 有限數字與標準數字字串為數值, 其餘字串為型別不符', function() {
358
+ ok(1.5, 'num', 1.5)
359
+ ok(12, 'num', 12)
360
+ ok('1.5', 'num', 1.5)
361
+ ok('-2', 'num', -2)
362
+ ok('1.254', 'num', 1.254)
363
+ for (let v of ['1.50', '.5', '5.', '1e3', ' 1', 'abc', '0x1']) {
364
+ no(v, 'num', 'mismatch')
365
+ }
366
+ })
367
+
368
+ it('R8 DECIMAL(表外細化): 小數位多於scale者寫入時會被四捨五入而改變鍵, 為型別不符', function() {
369
+ ok(1.25, 'num', 1.25, 2)
370
+ ok('1.25', 'num', 1.25, 2)
371
+ ok('0.5', 'num', 0.5, 2)
372
+ ok(3, 'num', 3, 0)
373
+ ok('-7', 'num', -7, 0)
374
+ for (let [v, sc] of [[1.254, 2], ['1.254', 2], [1.005, 2], [0.1 + 0.2, 2], [2.5, 0], ['2.5', 0]]) {
375
+ no(v, 'num', 'mismatch', sc)
376
+ }
377
+ })
378
+
379
+ it('R8 其他型別: 非空字串與有限數字原樣', function() {
380
+ ok('2020-01-01T00:00:00Z', 'date', '2020-01-01T00:00:00Z')
381
+ ok(5, 'any', 5)
382
+ ok('x', 'bool', 'x')
383
+ })
384
+
385
+ it('R8 任何型別: NaN、±Infinity、空字串、布林、物件、陣列、Date、BigInt、null、undefined為無效', function() {
386
+ for (let kind of ['str', 'int', 'num', 'date', 'any']) {
387
+ for (let v of [NaN, Infinity, -Infinity, '', Object(''), true, false, {}, [], [1], new Date(0), 10n, null, undefined]) {
388
+ no(v, kind, 'invalid')
389
+ }
390
+ }
391
+ })
392
+
393
+ })
@@ -1,113 +0,0 @@
1
- import fs from 'fs'
2
- import JSON5 from 'json5'
3
- import get from 'lodash-es/get.js'
4
- import each from 'lodash-es/each.js'
5
- import trim from 'lodash-es/trim.js'
6
- import join from 'lodash-es/join.js'
7
- import iseobj from 'wsemi/src/iseobj.mjs'
8
- import sep from 'wsemi/src/sep.mjs'
9
-
10
-
11
- function forJS(h) {
12
-
13
- // //j
14
- // let r = `define[\\s\\S]+}, {`
15
- // let reg = new RegExp(r, 'g')
16
- // let j = h.match(reg)[0]
17
-
18
- //s
19
- //let s = sep(j, '\n')
20
- let s = sep(h, '\n')
21
- // console.log('s', s)
22
-
23
- //find ind
24
- let indIDs = null
25
- let indIDe = null
26
- let indHasPK = false
27
- each(s, (v, k) => {
28
- v = trim(v)
29
- if (v === `'id': {`) {
30
- indIDs = k
31
- }
32
- if (indIDs !== null && indIDe === null && v.indexOf('primaryKey: true') >= 0) {
33
- indHasPK = true
34
- }
35
- if (indIDs !== null && v === '}') {
36
- indIDe = k
37
- }
38
- })
39
-
40
- //add primaryKey
41
- //console.log(`indIDs=${indIDs}, indIDe=${indIDe}, indHasPK=${indHasPK}`)
42
- if (indIDs !== null && !indHasPK) {
43
- //若id沒有被設定為pk, 則需要強制設為pk, 否則sequelize無法匯入
44
- s[indIDs] += 'primaryKey: true,' //用json5轉不用考慮是否最末不補逗號
45
- // if (indIDs + 2 < indIDe) {
46
- // //不只id欄位故要添加結尾逗號
47
- // s[indIDs] += ','
48
- // }
49
- }
50
-
51
- //c
52
- let c = join(s, '\n')
53
-
54
- //replace
55
- //let h2 = h.replace(reg, c)
56
- let h2 = c
57
- // console.log('h2', h2)
58
-
59
- return h2
60
- }
61
-
62
- function forJSON(h) {
63
-
64
- //s
65
- let s = JSON5.parse(h)
66
-
67
- //check
68
- if (!iseobj(get(s, 'fields'))) {
69
- console.log('invalid fields')
70
- return
71
- }
72
-
73
- //自動針對id設定為主鍵
74
- each(s.fields, (v, k) => {
75
- if (k === 'id') {
76
- v.primaryKey = true
77
- }
78
- })
79
-
80
- let h2 = JSON5.stringify(s, null, 4)
81
- // console.log('h2', h2)
82
-
83
- return h2
84
- }
85
-
86
- function modifyModel(fn, opt = {}) {
87
-
88
- //type
89
- let type = get(opt, 'type')
90
- if (type !== 'js' && type !== 'json') {
91
- type = 'js'
92
- }
93
-
94
- //h
95
- let h = fs.readFileSync(fn, 'utf8')
96
-
97
- //check
98
- let h2 = h
99
- if (type === 'js') {
100
- h2 = forJS(h)
101
- }
102
- else if (type === 'json') {
103
- h2 = forJSON(h)
104
- }
105
- // console.log('h2', h2)
106
-
107
- //write
108
- fs.writeFileSync(fn, h2, 'utf8')
109
-
110
- }
111
-
112
-
113
- export default modifyModel