w-orm-reladb 1.1.6 → 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.
@@ -1,117 +1,183 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import JSON5 from 'json5'
4
- import Sequelize from 'sequelize'
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import JSON5 from 'json5'
4
+ import Sequelize from 'sequelize'
5
5
  import each from 'lodash-es/each.js'
6
6
  import get from 'lodash-es/get.js'
7
7
  import isBoolean from 'lodash-es/isBoolean.js'
8
- import genPm from 'wsemi/src/genPm.mjs'
9
- import modifyModel from './modifyModel.mjs'
10
- import requireModel from './requireModel.js'
11
-
12
-
13
- function readJsModel(fn, sequelize) {
14
- //model = sequelize.import(fn) //舊版sequelize才有import函數
15
- //model = require(fn)(sequelize, Sequelize.DataTypes)
16
- let model = requireModel(fn, sequelize, Sequelize.DataTypes)
17
- return model
18
- }
19
-
20
- function readJsonModel(fn, sequelize) {
21
-
22
- //s
23
- let j = fs.readFileSync(fn, 'utf8')
24
- let s = JSON5.parse(j)
25
-
26
- //抽換DataTypes, 例如將'DataTypes.TEXT'改為DataTypes.TEXT
27
- each(s.fields, (v) => {
28
- v.type = get(Sequelize, v.type, null)
29
- })
30
-
31
- //model, 使用define產生model
32
- let model = sequelize.define(s.table, s.fields, s.options)
33
- // console.log('model', model)
34
-
35
- return model
36
- }
37
-
38
- async function importModels(fdModels, sequelize, name, opt = {}) {
39
-
40
- //type
41
- let type = get(opt, 'type')
42
- if (type !== 'js' && type !== 'json') {
43
- type = 'js'
44
- }
45
-
46
- //sync
47
- let sync = get(opt, 'sync')
48
- if (!isBoolean(sync)) {
49
- sync = false
50
- }
51
-
52
- //pm
53
- let pm = genPm()
54
-
55
- //file
56
- let file = `${name}.${type}`
57
-
58
- //fparent
59
- let fparent = path.resolve(fdModels) + path.sep
60
-
61
- //fn
62
- let fn = fparent + file
63
- // console.log('fn', fn)
64
-
65
- //modifyModel
66
- try {
67
- modifyModel(fn, { type })
68
- }
69
- catch (err) {
70
- pm.reject(err)
71
- return pm
72
- }
73
-
74
- //import
75
- let model
76
- try {
77
- if (type === 'js') {
78
- model = readJsModel(fn, sequelize)
79
- }
80
- else if (type === 'json') {
81
- model = readJsonModel(fn, sequelize)
82
- }
83
- }
84
- catch (err) {
85
- pm.reject(err)
86
- return pm
87
- }
88
-
89
- //sync
90
- try {
91
- if (sync) {
92
- await model.sync() //{ force: true }
93
- }
94
- }
95
- catch (err) {
96
- pm.reject(err)
97
- return pm
98
- }
99
-
100
- //models
101
- let models = {}
102
- try {
103
- models[model.name] = model
104
- }
105
- catch (err) {
106
- pm.reject(err)
107
- return pm
108
- }
109
-
110
- //resolve
111
- pm.resolve(models)
112
-
113
- return pm
114
- }
115
-
116
-
117
- export default importModels
8
+ import genPm from 'wsemi/src/genPm.mjs'
9
+ import requireModel from './requireModel.js'
10
+
11
+
12
+ //匯入資料表設定檔(models)
13
+ //
14
+ //model內之id欄位須為主鍵, 否則sequelize拒絕匯入(model.js: A column called 'id' was added ... but not marked with 'primaryKey: true')
15
+ //原版以modifyModel於[每一次]匯入時無條件重寫呼叫端之model原始檔(逐行trim、刪空行並改為LF)以補上主鍵設定,
16
+ //而本套件每次操作(含select、selectByPk)皆匯入一次, 故讀取函數亦改變可被觀察之狀態(規格T9),
17
+ //且多行程併發時一方正截斷檔案而另一方讀取, 即讀到空內容並寫回空檔而[永久損毀](已實機重現: 4行程併發, 4/4輪model檔被截為0位元組)
18
+ //
19
+ //本版改於記憶體內補上主鍵設定, 一律不寫model檔:
20
+ // js格式於呼叫model之定義函數期間暫時包裝sequelize.define, 於定義前補上id之primaryKey
21
+ // json格式於解析後、define前補上
22
+
23
+
24
+ /**
25
+ * 令欄位定義之id為主鍵, 不改動傳入之物件
26
+ * 註: 僅於model完全沒有主鍵設定而有id欄時補上(sequelize於此情形拒絕匯入);
27
+ * model已有其他主鍵時不改動, 否則成為(既有主鍵, id)之複合主鍵而使主鍵去重失效(原版modifyModel有此缺陷)
28
+ *
29
+ * @ignore
30
+ * @param {Object} attrs 輸入欄位定義物件
31
+ * @returns {Object} 回傳欄位定義物件
32
+ */
33
+ function patchIdPk(attrs) {
34
+
35
+ //check
36
+ if (!attrs || !Object.prototype.hasOwnProperty.call(attrs, 'id')) {
37
+ return attrs
38
+ }
39
+
40
+ //check, 已有任一主鍵設定者不改動
41
+ let hasPk = Object.keys(attrs).some((k) => get(attrs[k], 'primaryKey') === true)
42
+ if (hasPk) {
43
+ return attrs
44
+ }
45
+
46
+ //id
47
+ let id = attrs.id
48
+
49
+ //idNew, id可為欄位設定物件或直接為型別
50
+ let idNew = Object.prototype.toString.call(id) === '[object Object]' && get(id, 'type') !== undefined
51
+ ? { ...id, primaryKey: true }
52
+ : { type: id, primaryKey: true }
53
+
54
+ return { ...attrs, id: idNew }
55
+ }
56
+
57
+
58
+ /**
59
+ * 讀取js格式之model
60
+ * 註: 於呼叫model之定義函數期間暫時以實例自有屬性包裝define, 結束後刪除該屬性即回復原型上之define
61
+ *
62
+ * @ignore
63
+ */
64
+ function readJsModel(fn, sequelize) {
65
+ let define = sequelize.define
66
+ sequelize.define = function(name, attrs, options) {
67
+ return define.call(sequelize, name, patchIdPk(attrs), options)
68
+ }
69
+ try {
70
+ return requireModel(fn, sequelize, Sequelize.DataTypes)
71
+ }
72
+ finally {
73
+ delete sequelize.define
74
+ }
75
+ }
76
+
77
+
78
+ /**
79
+ * 讀取json格式之model
80
+ *
81
+ * @ignore
82
+ */
83
+ function readJsonModel(fn, sequelize) {
84
+
85
+ //s
86
+ let j = fs.readFileSync(fn, 'utf8')
87
+ let s = JSON5.parse(j)
88
+
89
+ //抽換DataTypes, 例如將'DataTypes.TEXT'改為DataTypes.TEXT
90
+ each(s.fields, (v) => {
91
+ v.type = get(Sequelize, v.type, null)
92
+ })
93
+
94
+ //model, 使用define產生model, 定義前於記憶體內補上id之主鍵設定
95
+ let model = sequelize.define(s.table, patchIdPk(s.fields), s.options)
96
+
97
+ return model
98
+ }
99
+
100
+
101
+ /**
102
+ * 匯入資料表設定檔
103
+ *
104
+ * 註: 不寫入model檔, id之主鍵設定於記憶體內補上
105
+ *
106
+ * @param {String} fdModels 輸入models所在資料夾字串
107
+ * @param {Object} sequelize 輸入sequelize實例
108
+ * @param {String} name 輸入資料表名稱字串
109
+ * @param {Object} [opt={}] 輸入設定物件
110
+ * @param {String} [opt.type='js'] 輸入資料表設定檔類型字串,可有'js'、'json'
111
+ * @param {Boolean} [opt.sync=false] 輸入是否將models資料同步至資料庫
112
+ * @returns {Promise} 回傳Promise,resolve回傳models物件,reject回傳錯誤訊息
113
+ */
114
+ async function importModels(fdModels, sequelize, name, opt = {}) {
115
+
116
+ //type
117
+ let type = get(opt, 'type')
118
+ if (type !== 'js' && type !== 'json') {
119
+ type = 'js'
120
+ }
121
+
122
+ //sync
123
+ let sync = get(opt, 'sync')
124
+ if (!isBoolean(sync)) {
125
+ sync = false
126
+ }
127
+
128
+ //pm
129
+ let pm = genPm()
130
+
131
+ //file
132
+ let file = `${name}.${type}`
133
+
134
+ //fparent
135
+ let fparent = path.resolve(fdModels) + path.sep
136
+
137
+ //fn
138
+ let fn = fparent + file
139
+
140
+ //import
141
+ let model
142
+ try {
143
+ if (type === 'js') {
144
+ model = readJsModel(fn, sequelize)
145
+ }
146
+ else if (type === 'json') {
147
+ model = readJsonModel(fn, sequelize)
148
+ }
149
+ }
150
+ catch (err) {
151
+ pm.reject(err)
152
+ return pm
153
+ }
154
+
155
+ //sync
156
+ try {
157
+ if (sync) {
158
+ await model.sync() //{ force: true }
159
+ }
160
+ }
161
+ catch (err) {
162
+ pm.reject(err)
163
+ return pm
164
+ }
165
+
166
+ //models
167
+ let models = {}
168
+ try {
169
+ models[model.name] = model
170
+ }
171
+ catch (err) {
172
+ pm.reject(err)
173
+ return pm
174
+ }
175
+
176
+ //resolve
177
+ pm.resolve(models)
178
+
179
+ return pm
180
+ }
181
+
182
+
183
+ export default importModels
@@ -0,0 +1,216 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import assert from 'assert'
4
+ import { spawn } from 'child_process'
5
+ import wo from '../src/WOrmReladb.mjs'
6
+
7
+
8
+ //rulebook R9: 各操作不得改寫呼叫端之model檔(規格T9讀取函數不得有副作用); 需補主鍵設定者只補該處、保留行尾並以原子寫入完成
9
+ //僅以sqlite驗證, 因model檔之處理與資料庫種類無關
10
+
11
+
12
+ let fd = './test/_tmp/api-models'
13
+
14
+
15
+ //genModel, 含縮排、空行與CRLF之model, 修正前之modifyModel會逐行trim、刪空行並改為LF而改寫
16
+ let genModel = (withPk) => {
17
+ return [
18
+ '/* jshint indent: 1 */',
19
+ 'module.exports = function(sequelize, DataTypes) {',
20
+ ' return sequelize.define(\'users\', {',
21
+ ' \'id\': {',
22
+ ' type: DataTypes.STRING,',
23
+ ' allowNull: false,',
24
+ withPk ? ' primaryKey: true,' : ' autoIncrement: false,',
25
+ ' },',
26
+ '',
27
+ ' \'name\': {',
28
+ ' type: DataTypes.TEXT,',
29
+ ' allowNull: true',
30
+ ' },',
31
+ ' \'value\': {',
32
+ ' type: DataTypes.FLOAT,',
33
+ ' allowNull: true',
34
+ ' }',
35
+ ' }, {',
36
+ ' tableName: \'users\'',
37
+ ' })',
38
+ '}',
39
+ '',
40
+ ].join('\r\n')
41
+ }
42
+
43
+
44
+ let runWorker = (fdModels, storage, n) => {
45
+ return new Promise((resolve) => {
46
+ let p = spawn(process.execPath, ['test/tools/modelsWorker.mjs', fdModels, storage, String(n)], { stdio: ['ignore', 'pipe', 'pipe'] })
47
+ let s = ''
48
+ p.stdout.on('data', (d) => {
49
+ s += d
50
+ })
51
+ p.stderr.on('data', () => {})
52
+ p.on('close', () => {
53
+ let line = s.trim().split('\n').filter((v) => v.startsWith('{')).pop()
54
+ resolve(line ? JSON.parse(line) : { nOk: 0, nErr: -1, raw: s })
55
+ })
56
+ })
57
+ }
58
+
59
+
60
+ describe('models file', function() {
61
+
62
+ before(function() {
63
+ fs.mkdirSync(fd, { recursive: true })
64
+ })
65
+
66
+ after(function() {
67
+ fs.rmSync(fd, { recursive: true, force: true })
68
+ })
69
+
70
+ it('已具主鍵設定之model經各函數與createStorage後位元組與mtime不變', async function() {
71
+ let fdm = path.join(fd, 'm1')
72
+ fs.mkdirSync(fdm, { recursive: true })
73
+ let fn = path.join(fdm, 'users.js')
74
+ fs.writeFileSync(fn, genModel(true), 'utf8')
75
+ let b0 = fs.readFileSync(fn)
76
+ let t0 = fs.statSync(fn).mtimeMs
77
+ await new Promise((resolve) => setTimeout(resolve, 30))
78
+
79
+ let w = wo({ url: 'sqlite://:', storage: path.join(fd, 'm1.sqlite'), db: 'worm', cl: 'users', fdModels: fdm })
80
+ await w.createStorage()
81
+ await w.select()
82
+ await w.selectByPk('k1')
83
+ await w.insert({ id: 'k1', name: 'a' })
84
+ await w.insertBulk({ id: 'k2', name: 'b' })
85
+ await w.save({ id: 'k1', name: 'c' })
86
+ await w.del({ id: 'k2' })
87
+ await w.delAll({ id: 'k1' })
88
+
89
+ assert.ok(fs.readFileSync(fn).equals(b0), 'model檔內容被改寫')
90
+ assert.strictEqual(fs.statSync(fn).mtimeMs, t0, 'model檔被重寫(mtime改變)')
91
+ })
92
+
93
+ it('缺主鍵設定之model亦不寫檔, id於記憶體內視為主鍵', async function() {
94
+ //rulebook R9: 主鍵設定於記憶體內補上, 讀取與寫入函數皆不改寫model檔(含第一次讀取)
95
+ let fdm = path.join(fd, 'm2')
96
+ fs.mkdirSync(fdm, { recursive: true })
97
+ let fn = path.join(fdm, 'users.js')
98
+ fs.writeFileSync(fn, genModel(false), 'utf8')
99
+ let b0 = fs.readFileSync(fn)
100
+ let t0 = fs.statSync(fn).mtimeMs
101
+ await new Promise((resolve) => setTimeout(resolve, 30))
102
+
103
+ let w = wo({ url: 'sqlite://:', storage: path.join(fd, 'm2.sqlite'), db: 'worm', cl: 'users', fdModels: fdm })
104
+ await w.select().catch(() => {}) //尚未建表之第一次讀取
105
+ await w.createStorage()
106
+ await w.insert({ id: 'k1', name: 'a' })
107
+ let msg = await w.insert({ id: 'k1', name: 'b' })
108
+ assert.deepStrictEqual(msg, { n: 1, nInserted: 0, ok: 1 }, 'id未被視為主鍵')
109
+ assert.deepStrictEqual(await w.selectByPk('k1'), { id: 'k1', name: 'a', value: null })
110
+
111
+ assert.ok(fs.readFileSync(fn).equals(b0), 'model檔內容被改寫')
112
+ assert.strictEqual(fs.statSync(fn).mtimeMs, t0, 'model檔被重寫(mtime改變)')
113
+ assert.deepStrictEqual(fs.readdirSync(fdm), ['users.js'])
114
+ })
115
+
116
+ it('model之defaultScope帶limit時delAll拒絕且不刪除', async function() {
117
+ //rulebook R1: select依scope之order取前幾筆而sequelize之限量刪除不採order, 兩者對象分歧, 故delAll拒絕
118
+ let fdm = path.join(fd, 'm4')
119
+ fs.mkdirSync(fdm, { recursive: true })
120
+ let fn = path.join(fdm, 'users.js')
121
+ fs.writeFileSync(fn, genModel(true).replace('tableName: \'users\'', 'tableName: \'users\', defaultScope: { order: [[\'id\', \'DESC\']], limit: 1 }'), 'utf8')
122
+ let w = wo({ url: 'sqlite://:', storage: path.join(fd, 'm4.sqlite'), db: 'worm', cl: 'users', fdModels: fdm })
123
+ await w.createStorage()
124
+ await w.insert([{ id: 'a' }, { id: 'b' }])
125
+ let r = await w.delAll({}).then(() => null).catch((err) => String(err.message || err))
126
+ assert.ok(r !== null && r.indexOf('defaultScope') >= 0, `未reject: ${r}`)
127
+ let wAll = wo({ url: 'sqlite://:', storage: path.join(fd, 'm4.sqlite'), db: 'worm', cl: 'users', fdModels: path.join(fd, 'm1') })
128
+ assert.deepStrictEqual((await wAll.select()).map((v) => v.id).sort(), ['a', 'b'])
129
+ })
130
+
131
+ it('BIGINT主鍵: 超出安全整數之數值為型別不符, 精確值以標準整數寫法之字串給予', async function() {
132
+ //rulebook R8: 整數類主鍵之數值須為安全整數, 否則已失精度而會命中相鄰主鍵; 超出安全整數之標準整數寫法字串保留為字串(表外細化)
133
+ let fdm = path.join(fd, 'm5')
134
+ fs.mkdirSync(fdm, { recursive: true })
135
+ let fn = path.join(fdm, 'tbig.js')
136
+ fs.writeFileSync(fn, [
137
+ 'module.exports = function(sequelize, DataTypes) {',
138
+ ' return sequelize.define(\'tbig\', {',
139
+ ' \'id\': { type: DataTypes.BIGINT, allowNull: false, primaryKey: true },',
140
+ ' \'name\': { type: DataTypes.TEXT, allowNull: true },',
141
+ ' }, { tableName: \'tbig\' })',
142
+ '}',
143
+ ].join('\n'), 'utf8')
144
+ let w = wo({ url: 'sqlite://:', storage: path.join(fd, 'm5.sqlite'), db: 'worm', cl: 'tbig', fdModels: fdm, autoGenPk: false })
145
+ await w.createStorage()
146
+ await w.insert([{ id: '9007199254740992', name: 'even' }, { id: '9007199254740993', name: 'odd' }])
147
+ assert.strictEqual(await w.selectByPk(Number('9007199254740993')), null)
148
+ assert.strictEqual((await w.selectByPk('9007199254740993')).name, 'odd')
149
+ let md = await w.del({ id: Number('9007199254740993') })
150
+ assert.strictEqual(md[0].ok, 0)
151
+ let rd = await w.delAll({ id: Number('9007199254740993') }).then(() => null).catch((err) => String(err.message || err))
152
+ assert.ok(rd !== null && rd.indexOf('does not match column type [int]') >= 0, `delAll未reject: ${rd}`)
153
+ assert.deepStrictEqual((await w.select({ id: { $in: ['9007199254740992', '9007199254740993'] } })).map((v) => v.name).sort(), ['even', 'odd'])
154
+ })
155
+
156
+ it('model已有其他主鍵時不得把id補成主鍵(不得成為複合主鍵)', async function() {
157
+ //rulebook R9: 僅於model完全沒有主鍵設定時才補id; 否則(sid, id)成為複合主鍵而使主鍵去重失效
158
+ let fdm = path.join(fd, 'm6')
159
+ fs.mkdirSync(fdm, { recursive: true })
160
+ fs.writeFileSync(path.join(fdm, 'tsid.js'), [
161
+ 'module.exports = function(sequelize, DataTypes) {',
162
+ ' return sequelize.define(\'tsid\', {',
163
+ ' \'sid\': { type: DataTypes.STRING, allowNull: false, primaryKey: true },',
164
+ ' \'id\': { type: DataTypes.STRING, allowNull: true },',
165
+ ' }, { tableName: \'tsid\' })',
166
+ '}',
167
+ ].join('\n'), 'utf8')
168
+ let w = wo({ url: 'sqlite://:', storage: path.join(fd, 'm6.sqlite'), db: 'worm', cl: 'tsid', fdModels: fdm, pk: 'sid' })
169
+ await w.createStorage()
170
+ let msg = await w.insert([{ sid: 'a', id: 'x' }, { sid: 'a', id: 'y' }])
171
+ assert.deepStrictEqual(msg, { n: 2, nInserted: 1, ok: 1 }, '主鍵去重失效')
172
+ assert.deepStrictEqual(await w.del({ sid: 'a' }), [{ n: 1, nDeleted: 1, ok: 1 }])
173
+ })
174
+
175
+ it('INTEGER主鍵: 收安全整數與標準整數寫法之字串, 其他字串為型別不符', async function() {
176
+ //rulebook R8: 整數類主鍵收安全整數或標準整數寫法之字串(String(Number(s))===s, 轉為數值)
177
+ let fdm = path.join(fd, 'm7')
178
+ fs.mkdirSync(fdm, { recursive: true })
179
+ fs.writeFileSync(path.join(fdm, 'tint.js'), [
180
+ 'module.exports = function(sequelize, DataTypes) {',
181
+ ' return sequelize.define(\'tint\', {',
182
+ ' \'id\': { type: DataTypes.INTEGER, allowNull: false, primaryKey: true },',
183
+ ' \'name\': { type: DataTypes.TEXT, allowNull: true },',
184
+ ' }, { tableName: \'tint\' })',
185
+ '}',
186
+ ].join('\n'), 'utf8')
187
+ let w = wo({ url: 'sqlite://:', storage: path.join(fd, 'm7.sqlite'), db: 'worm', cl: 'tint', fdModels: fdm, autoGenPk: false })
188
+ await w.createStorage()
189
+ await w.insert([{ id: 1, name: 'a' }, { id: '2', name: 'b' }])
190
+ assert.strictEqual((await w.selectByPk(1)).name, 'a')
191
+ assert.strictEqual((await w.selectByPk('2')).name, 'b')
192
+ assert.strictEqual(await w.selectByPk('0x1'), null)
193
+ assert.strictEqual(await w.selectByPk(' 1'), null)
194
+ assert.strictEqual((await w.del({ id: 'abc' }))[0].ok, 0)
195
+ let r = await w.save({ id: '0x2', name: 'z' }).then(() => null).catch((err) => String(err.message || err))
196
+ assert.ok(r !== null && r.indexOf('type does not match pk column') >= 0, `save未reject: ${r}`)
197
+ assert.deepStrictEqual((await w.select()).map((v) => v.name).sort(), ['a', 'b'])
198
+ })
199
+
200
+ it('4行程併發操作後model檔不變且全部成功', async function() {
201
+ this.timeout(120000)
202
+ let fdm = path.join(fd, 'm3')
203
+ fs.mkdirSync(fdm, { recursive: true })
204
+ let fn = path.join(fdm, 'users.js')
205
+ fs.writeFileSync(fn, genModel(true), 'utf8')
206
+ let b0 = fs.readFileSync(fn)
207
+ let n = 50
208
+ let rs = await Promise.all([0, 1, 2, 3].map((k) => runWorker(fdm, path.join(fd, `m3-${k}.sqlite`), n)))
209
+ for (let r of rs) {
210
+ assert.strictEqual(r.nErr, 0, `子行程失敗: ${JSON.stringify(r)}`)
211
+ assert.strictEqual(r.nOk, n + 1)
212
+ }
213
+ assert.ok(fs.readFileSync(fn).equals(b0), `model檔被改寫, 現為${fs.statSync(fn).size}位元組`)
214
+ })
215
+
216
+ })