doix-db 1.0.73 → 1.0.75

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/README.md CHANGED
@@ -1,16 +1,228 @@
1
1
  ![workflow](https://github.com/do-/node-doix-db/actions/workflows/main.yml/badge.svg)
2
2
  ![Jest coverage](./badges/coverage-jest%20coverage.svg)
3
3
 
4
- # node-doix-db
5
- `doix-db` is a plug in for [doix](https://github.com/do-/node-doix) framework implementing a common interface to relational databases. It features:
6
- * [DbClient](https://github.com/do-/node-doix-db/wiki/DbClient) — the database API available to `doix` [Job](https://github.com/do-/node-doix/wiki/Job)s;
7
- * [DbModel](https://github.com/do-/node-doix-db/wiki/DbModel) — the set of classes representing the database structure;
8
- * [DbQuery](https://github.com/do-/node-doix-db/wiki/DbQuery) a `DbModel` based `SELECT` builder;
9
- * [DbMigrationPlan](https://github.com/do-/node-doix-db/wiki/DbMigrationPlan) — a `DbModel` based deployment automation tool;
10
- * [DbLang](https://github.com/do-/node-doix-db/wiki/DbLang) — a set of SQL generating functions for miscellaneous application tasks.
11
-
12
- Has backends for
13
- * [PostgreSQL](https://github.com/do-/node-doix-db-postgresql),
14
- * [ClickHouse](https://github.com/do-/node-doix-db-clickhouse).
4
+ `doix-db` is an extension to the [doix](https://github.com/do-/node-doix) framework for working with [relational databases](https://en.wikipedia.org/wiki/Relational_database).
5
+
6
+ # tl;dr
7
+
8
+ Start a trivial Web service project with [doix-http](https://github.com/do-/node-doix-http/wiki), add a connection to [PostgreSQL](https://github.com/do-/node-doix-db-postgresql/wiki#tldr) or to [ClickHouse](https://github.com/do-/node-doix-db-clickhouse/wiki#tldr) and hack on.
9
+
10
+ # Description
11
+
12
+ Basically, this is the common [RDB](https://en.wikipedia.org/wiki/Relational_database) interface for `doix`, the same as ODBC for Windows, JDBC for Java and so on.
13
+
14
+ Aside from processing given statements, it features some SQL generation capabilities.
15
+
16
+ # Connection & Basic Operation
17
+
18
+ For an [Application](https://github.com/do-/node-doix/wiki/Application) to operate on a database, you have to register therein the properly configured vendor specific [`DbPool`](https://github.com/do-/node-doix-db/wiki/DbPool):
19
+
20
+ ```js
21
+ const {DbPoolPg} = require ('doix-db-postgresql')
22
+ // const {DbPoolCh} = require ('doix-db-clickhouse')
23
+
24
+ const db = new DbPoolPg (conf.db)
25
+ // const dbArchive = new DbPoolCh (conf.dbArchive)
26
+
27
+ ///...
28
+ pools: {
29
+ db,
30
+ // dbArchive,
31
+ },
32
+ ///...
33
+ ```
34
+
35
+ Then, corresponding [`DbClient`](https://github.com/do-/node-doix-db/wiki/DbClient) instances will be automatically [injected](https://github.com/do-/node-doix/wiki#dependency-injection) in execution contexts:
36
+
37
+ ```js
38
+ const dt = await this.db.getScalar ('SELECT CURRENT_DATE')
39
+ // await this.dbArchive.do ('ALTER TABLE facts DROP PARTITION ?', [dt])
40
+ ```
41
+
42
+ Asynchronous methods can be called right away; initialization and cleanup are up to `doix` internals.
43
+
44
+ Just in case, the hosting application is always in hand, with all its internals, including the `pools` [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), so developers may operate on it directly, at their own risk:
45
+
46
+ ```js
47
+ this.app.pools.get ('db').pool.end () // see https://node-postgres.com/apis/pool#poolend
48
+ ```
49
+
50
+ ## Executing Arbitrary SQL
51
+ [`DbClient`](https://github.com/do-/node-doix-db/wiki/DbClient)'s most common methods are:
52
+ * [`do`](https://github.com/do-/node-doix-db/wiki/DbClient#do-q-p-options) for write only [DML](https://en.wikipedia.org/wiki/Data_manipulation_language)/[DDL](https://en.wikipedia.org/wiki/Data_definition_language) commands;
53
+ * [`get***` family](https://github.com/do-/node-doix-db/wiki/DbClient#data-fetching) for `SELECT` and other data returning requests.
54
+
55
+ The API is designed to be versatile yet concise. Each request is invoked by a single call; scalars, arrays and streams are represented uniformly.
56
+
57
+ In result sets, the primitive types mapping depends on the specific driver, but, in general, when ambiguous, strings are used. In particular:
58
+ * fixed precision numbers (`DECIMAL` etc.) are returned as [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)s, never by [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)s to avoid rounding errors;
59
+ * dates are represented by [ISO](https://en.wikipedia.org/wiki/ISO_8601) strings, never by [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)s, because of time zone related issues.
60
+
61
+ For bound parameters, contrarily, `doix-db` accepts nearly everything and do the best to map it properly in an intuitive way. It's recommended however to
62
+ provide all values but safe integers in the string form.
63
+
64
+ Like every process in `doix`, each SQL statement execution is transparently [logged](https://github.com/do-/node-doix/wiki#logging), with references to the containing [`job`](https://github.com/do-/node-doix/wiki/Job).
65
+
66
+ # Using a Database Model
67
+
68
+ Far from embracing the [MDA](https://en.wikipedia.org/wiki/Model-driven_architecture) approach in its totality, `doix-db` is developed keeping in mind that an application must be aware of, and effectively use meta information about data structures in operates on. Even more, a well designed application must keep the image of the _required_ structure of its database and should be able to compare it to the actual one, maybe outdated, and to _upgrade_ it according to requirements: primarily, with automatic migrations during deployments.
69
+
70
+ To this end, `doix-db` offers a means to load the presumed database schemata from source files next to the [`Application`](https://github.com/do-/node-doix/wiki/Application)'s modules.
71
+
72
+ ## Setting Up, Exploring Content
73
+ Each database object, such as a [table](https://github.com/do-/node-doix-db/wiki/DbTable), [sql view](https://github.com/do-/node-doix-db/wiki/DbView) and so on, must be described by an eponymous [module](https://nodejs.org/api/modules.html). Suppose you have all necessary modules in a directory called` /path/to/the/model`. To make use of this information, you have to associate it with the corresponding pool on application start (here, we suppose the `db` and `dbArchive` variables are the same as in the example above):
74
+
75
+ ```js
76
+ const {DbModel} = require ('doix-db')
77
+
78
+ // simple case:
79
+ new DbModel ({db, src: [{root: `/path/to/the/model`}]}).loadModules ()
80
+
81
+ // advanced usage:
82
+ {
83
+ const dbModel = new DbModel ({dbArchive, src: [{root: `/path/to/the/model`}]})
84
+ // dbModel.on ('objects-created', function () {/*...*/}) // after resolveReferences ()
85
+ // dbModel.prependListener ('objects-created', function () {/*...*/}) // before
86
+ dbModel.loadModules ()
87
+ }
88
+ ```
89
+
90
+ Now both pools, and each [`DbClient`](https://github.com/do-/node-doix-db/wiki/DbClient) acquired, will have corresponding [`.model`](https://github.com/do-/node-doix-db/wiki/DbModel) properties injected therein. For simple tasks, you can use them like this:
91
+
92
+ ```js
93
+ const {data: {length}} = this.db.model.find ('dim.gender') // known table, mandatory fixed content
94
+ this.logger.log ({message: `We suppose there are ${length} genders`})
95
+
96
+ const tmpTables = []; for (const o of this.db.model.objects) // iterate over schemata
97
+ if (o instanceof DbTable && o.isToPurge) // check for a custom tag
98
+ tmpTables.push (o.qName)
99
+ if (tmpTables.length !== 0)
100
+ await db.do (`TRUNCATE ${tmpTables}`)
101
+ ```
102
+ More advanced API is covered below.
103
+
104
+ ## NOSQL: Not Only SQL
105
+ ### Document Oriented API
106
+ Given a table name and its primary key value(s), you can fetch the single record without coding SQL:
107
+
108
+ ```js
109
+ const user = await this.db.getObject ('users', [id]
110
+ // , {notFound: {id: 0}} // fail safe demo, just in case
111
+ )
112
+ ```
113
+
114
+ It may look similar to some other frameworks functionality, namely [Sequelize](https://sequelize.org/docs/v6/core-concepts/model-querying-finders/#findbypk), but let's underline that `doix-db` <u>is by no means an</u> [ORM](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) framework. In this section, records are represented by plain objects, not strictly typed ones. No such ORMish things here as _eager/lazy loading_, _detached state_ etc.
115
+
116
+ To save a given record in a given table, [insert](https://github.com/do-/node-doix-db/wiki/DbClient#insert-tablename-records-options), [update](https://github.com/do-/node-doix-db/wiki/DbClient#update-tablename-record-options) and [upsert](https://github.com/do-/node-doix-db-postgresql/wiki/DbClientPg#upsert-tablename-record-options) are available:
117
+
118
+ ```js
119
+ await this.db.insert ('log', {id: 1, message: 'Test', level: 1}, {
120
+ // onlyIfMissing: true // ON CONFLICT DO NOTHING, PostgreSQL only
121
+ // result: 'record' // what to return, PostgreSQL too
122
+ })
123
+ await this.db.update ('log', {id: 1, message: 'The test'})
124
+ await this.db.upsert ('user_options', {id_user: 1, id_option: 10, value: true}, {
125
+ // key: ['id_user', 'id_option'] // if differs from the primary key
126
+ })
127
+ ```
128
+ Naturally, field names must match. Extra properties unknown to the data model are silently ignored. Although not recommended for mission critical high load operations, this technique can save a lot of time while prototyping a simple CRUD functionality.
129
+
130
+ For developer's convenience, the umbrella `insert` method, aside from processing single records, features the mass loading. It accepts arrays:
131
+ ```js
132
+ await this.db.insert ('log', [
133
+ {id: 2, message: 'Two'},
134
+ {id: 3, message: 'Three'},
135
+ ])
136
+ ```
137
+ and [object streams](https://nodejs.org/api/stream.html#object-mode):
138
+ ```js
139
+ const objectStream = await this.db.getStream (`SELECT * FROM vw_sales_to_archive`)
140
+ await this.dbArchive.insert ('sales', objectStream)
141
+ ```
142
+ For maximal performance, native database streams are used whenever possible.
143
+
144
+ ### Dynamic Search Queries
145
+
146
+ Most user interfaces (especially, Web ones) contain scrollable roasters with multiple search fields. Commonly, most of filters are unset by default, and empty values must be ignored.
147
+
148
+ In this situation, the SQL query is better generated based on the actual set of search terms requested: at least the `WHERE` clause, but quite often the `FROM` part too, may be some others. And to display a page counter, a secondary `SELECT COUNT(*)` is needed, with a specific optimization (excluding `ORDER BY`, omitting some `OUTER JOIN`s, etc.)
149
+
150
+ To facilitate the code generation is most such cases, `doix-db` features a [dynamic query builder](https://github.com/do-/node-doix-db/wiki/DbQuery). Unlike some well known analogs (e. g. [Knex.js](https://knexjs.org/guide/query-builder.html)), its API doesn't mock a SQL AST in form of multiple chained method calls.
151
+
152
+ From the application perspective, given a set of filters in form of a plain object, it takes a single [`this.db.model.createQuery ()`](https://github.com/do-/node-doix-db/wiki/DbModel#createquery) call to obtain a builder instance and immediately pass it as a parameter to [this.db.getArray ()](https://github.com/do-/node-doix-db/wiki/DbClient#getarray-q-p-options).
153
+
154
+ ```js
155
+ const q = this.db.model.createQuery (
156
+ [['notes', {filters: [['txt', 'ILIKE', '%' + v + '%']]}]],
157
+ {order: 'created', limit: 50, offset: 0}
158
+ )
159
+ , range = await this.db.getArray (q) // LIMIT / OFFSET applied
160
+ , count = selection [Symbol.for ('count')] // extra COUNT(*) result
161
+ ```
162
+
163
+ But what is the right structure for that _set of filters_ mentioned above? Surprisingly, no common standard for this is in sight. Every DHTML AJAX library featuring some advanced data grid seems to invent its own one: the same thing once expressed as `filter: ['label', 'startswith', 'admin']`, looks like `search: [{field: 'label', operator: 'begins', value: 'admin'}]` elsewhere and so on.
164
+
165
+ They are all pretty similar though, those micro languages. No problem to translate from one to another, more versatile one (the `doix-db` one, that is). Two translators of this kind are available: [for DevExtreme](https://github.com/do-/node-doix-devextreme) and [for w2ui](https://github.com/do-/node-doix-w2ui) frameworks. More can be cloned easily.
166
+
167
+ ## Planning and Performing Migrations
168
+
169
+ The aforementioned [Sequelize](https://sequelize.org/docs/v6/other-topics/migrations/#running-migrations) and [Knex.js](https://knexjs.org/guide/migrations.html) both offer tools to automate database migrations, very similar to ones implemented by [Liquibase](https://www.npmjs.com/package/liquibase) and [Flyway](https://www.npmjs.com/package/node-flywaydb) both ported from the Java universe. In all those cases, developers code migration steps in an imperative manner, and the framework assembles those fragments in a sequence based on initial and final version numbers.
170
+
171
+ `doix-db` takes a completely different approach to the same problem. It features [`DbMigrationPlan`](https://github.com/do-/node-doix-db/wiki/DbMigrationPlan): a diff/patch like tool comparing the actual physical database structure to the required one and generating necessary DDL statements.
172
+
173
+ ```js
174
+ const plan = this.db.createMigrationPlan ()
175
+
176
+ // plan.on (..., ...) // ...set custom event handlers
177
+
178
+ await plan.loadStructure () // read the INFORMATION_SCHEMA
179
+
180
+ // ...adjust something, based on plan.asIs
181
+
182
+ plan.inspectStructure () // compare with this.db.model
183
+
184
+ // ...adjust something more, based on plan.toDo
185
+
186
+ await this.db.doAll (plan.genDDL ()) // execute (or maybe just record plan.genDDL ())
187
+ ```
188
+
189
+ So, in essence, `doix-db`'s `DbMigrationPlan` vs. Liquibase like tools is [closed loop vs. open loop](https://en.wikipedia.org/w/index.php?title=Control_loop&oldid=1281798171#Open-loop_and_closed-loop) control.
190
+
191
+ ## Ensuring Fixed Data
192
+
193
+ It's quite a common case to have several relational tables only filled up with few known records each. These are dictionaries of values like system roles, document status etc. They need to be kept in the database to be available for `JOIN`s, but in fact are application constants.
194
+
195
+ In `doix-db`, such fixed content is normally added to [table definitions](https://github.com/do-/node-doix-db/wiki/DbTable)
196
+
197
+ ```js
198
+ ...
199
+ data: [
200
+ {id: 1, name: 'admin'},
201
+ {id: 2, name: 'user'},
202
+ ],
203
+ ...
204
+ ```
205
+ what guarantees it to be present in the table (via [`DbMigrationPlan`](https://github.com/do-/node-doix-db/wiki/DbMigrationPlan)) and makes it visible to the application code.
206
+
207
+ When developing AJAX backends, a frequent task is to augment an object representing a data record with some dictionaries for its fields. For sure such data must be taken right from the application's memory rather than fetched from the database:
208
+
209
+ ```js
210
+ const user = await this.db.getObject ('users', [id])
211
+ user.roles = this.db.model.find ('roles').data
212
+ user.status = this.db.model.find ('status').data
213
+ //...
214
+ return user
215
+ ```
216
+
217
+ And there is some [API sugar](https://github.com/do-/node-doix-db/wiki/DbModel#assigndata) for this:
218
+ ```js
219
+ return this.db.model.assignData (
220
+ await this.db.getObject ('users', [id]),
221
+ [
222
+ 'roles',
223
+ 'status',
224
+ ]
225
+ )
226
+ ```
15
227
 
16
228
  More information is available at https://github.com/do-/node-doix-db/wiki
@@ -59,6 +59,8 @@ class DbCsvPrinter extends Transform {
59
59
 
60
60
  if (!Array.isArray (columns)) columns = Object.entries (columns).map (([name, def]) => {
61
61
 
62
+ if (typeof def === 'string') def = this.lang.parseColumn (def)
63
+
62
64
  const col = new DbColumn (def)
63
65
 
64
66
  col.name = name
package/lib/DbLang.js CHANGED
@@ -14,6 +14,17 @@ const DbTypeCharacter = require ('./model/types/DbTypeCharacter.js')
14
14
  const DbTypeDate = require ('./model/types/DbTypeDate.js')
15
15
  const DbTypeTimestamp = require ('./model/types/DbTypeTimestamp.js')
16
16
 
17
+ const CH_ROUND_CLOSE = ')'.charCodeAt (0)
18
+ const CH_SQUARE_CLOSE = ']'.charCodeAt (0)
19
+ const CH_CURLY_OPEN = '{'.charCodeAt (0)
20
+ const CH_CURLY_CLOSE = '}'.charCodeAt (0)
21
+ const CH_SLASH = '/'.charCodeAt (0)
22
+ const CH_BACK_SLASH = '\\'.charCodeAt (0)
23
+ const CH_QUESTION = '?'.charCodeAt (0)
24
+ const CH_EXCLAMATION = '!'.charCodeAt (0)
25
+
26
+ const RE_INT = /^[1-9][0-9]*$/
27
+
17
28
  const CH_Q = "'", CH_QQ = '"'
18
29
 
19
30
  const Q_ESC = new stringEscape ([
@@ -93,6 +104,30 @@ class DbLang {
93
104
 
94
105
  }
95
106
 
107
+ createDbObject (options) {
108
+
109
+ {
110
+
111
+ const {columns} = options; if (columns)
112
+
113
+ for (const name in columns)
114
+
115
+ if (typeof columns [name] === 'string')
116
+
117
+ columns [name] = this.parseColumn (columns [name])
118
+
119
+ }
120
+
121
+ const clazz = this.getDbObjectClass (options)
122
+
123
+ const dbObject = new clazz (options)
124
+
125
+ dbObject.setLang (this)
126
+
127
+ return dbObject
128
+
129
+ }
130
+
96
131
  getDbObjectClassesToDiscover () {
97
132
 
98
133
  return [DbTable]
@@ -548,6 +583,183 @@ class DbLang {
548
583
 
549
584
  }
550
585
 
586
+ parseColumn (src) {
587
+
588
+ const o = {src: src.trim ()}
589
+
590
+ try {
591
+
592
+ this.parseColumnComment (o)
593
+ this.parseColumnExtra (o)
594
+ this.parseColumnPattern (o)
595
+ this.parseColumnRange (o)
596
+ this.parseColumnDefault (o)
597
+
598
+ if (o.src.length === 0) throw Error (`Cannot determine the type`)
599
+
600
+ this.parseColumnNullable (o)
601
+
602
+ o.isReference = o.type.startsWith ('(')
603
+
604
+ if (!o.isReference) {
605
+ this.parseColumnDimension (o)
606
+ }
607
+
608
+ o.src = src
609
+
610
+ return o
611
+
612
+ }
613
+ catch (cause) {
614
+
615
+ if (o.src.length === 0) throw Error (`Invalid column definition: '${src}'`, {cause})
616
+
617
+ }
618
+
619
+ }
620
+
621
+ parseColumnComment (o) {
622
+
623
+ const {src} = o, pos = src.lastIndexOf ('//'); if (pos < 0) return
624
+
625
+ o.comment = src.substring (pos + 2).trimStart ()
626
+
627
+ o.src = src.substring (0, pos).trimEnd ()
628
+
629
+ }
630
+
631
+ parseColumnExtra (o) {
632
+
633
+ const {src} = o
634
+
635
+ let pos = src.length - 1; if (src.charCodeAt (pos) !== CH_CURLY_CLOSE) return
636
+
637
+ let level = 1; while (level !== 0) {
638
+
639
+ if (-- pos < 0) throw Error ('Unbalanced "}"')
640
+
641
+ switch (src.charCodeAt (pos)) {
642
+
643
+ case CH_CURLY_CLOSE:
644
+ level ++
645
+ break
646
+
647
+ case CH_CURLY_OPEN:
648
+ level --
649
+ break
650
+
651
+ }
652
+
653
+ }
654
+
655
+ for (const [k, v] of Object.entries (new Function ('return ' + src.substring (pos)) ())) o [k] = v
656
+
657
+ o.src = src.substring (0, pos).trimEnd ()
658
+
659
+ }
660
+
661
+ parseColumnPattern (o) {
662
+
663
+ const {src} = o
664
+
665
+ let pos = src.length - 1; if (src.charCodeAt (pos) !== CH_SLASH) return
666
+
667
+ while (true) {
668
+
669
+ pos = src.lastIndexOf ('/', pos - 1); if (pos < 0) throw Error (`Cannot find opening '/' for the pattern`)
670
+
671
+ if (src.charCodeAt (pos - 1) !== CH_BACK_SLASH) break
672
+
673
+ }
674
+
675
+ o.pattern = src.slice (pos + 1, -1)
676
+
677
+ o.src = src.substring (0, pos).trimEnd ()
678
+
679
+ }
680
+
681
+ parseColumnRange (o) {
682
+
683
+ const {src} = o
684
+
685
+ if (src.charCodeAt (src.length - 1) !== CH_SQUARE_CLOSE) return
686
+
687
+ const begin = src.lastIndexOf ('['); if (begin < 0) throw Error (`Cannot find the opening '[' for range`)
688
+
689
+ const range = src.slice (begin + 1, -1)
690
+
691
+ o.src = src.substring (0, begin).trimEnd ()
692
+
693
+ const pos = range.indexOf ('..'); if (pos < 0) throw Error (`Cannot find '..' between '[' and ']'`)
694
+
695
+ const min = range.substring (0, pos).trim (); if (min.length !== 0) o.min = min
696
+
697
+ const max = range.substring (pos + 2).trim (); if (max.length !== 0) o.max = max
698
+
699
+ }
700
+
701
+ parseColumnDefault (o) {
702
+
703
+ const {src} = o, pos = src.indexOf ('='); if (pos < 0) return
704
+
705
+ o.default = src.substring (pos + 1).trimStart ()
706
+
707
+ if (o.default.length === 0) throw Error (`Empty (but not NULL neither '') default definition`)
708
+
709
+ o.src = src.substring (0, pos).trimEnd ()
710
+
711
+ }
712
+
713
+ parseColumnNullable (o) {
714
+
715
+ let nullable = !('default' in o)
716
+
717
+ const {src} = o, {length} = src, override = nullable ? CH_EXCLAMATION : CH_QUESTION
718
+
719
+ if (src.charCodeAt (length - 1) === override) {
720
+
721
+ nullable = !nullable
722
+
723
+ o.src = src.slice (0, -1).trim ()
724
+
725
+ }
726
+
727
+ o.nullable = nullable
728
+
729
+ o.type = o.src
730
+
731
+ }
732
+
733
+ parseColumnDimension (o) {
734
+
735
+ const src = o.type
736
+
737
+ if (src.charCodeAt (src.length - 1) !== CH_ROUND_CLOSE) return
738
+
739
+ const begin = src.lastIndexOf ('('); if (begin < 0) throw Error (`Cannot find opening '(' for dimension`)
740
+
741
+ let dimension = src.slice (begin + 1, -1)
742
+
743
+ o.type = src.slice (0, begin).trim ()
744
+
745
+ const pos = dimension.indexOf (','); if (pos >= 0) {
746
+
747
+ const scale = dimension.slice (pos + 1).trim ()
748
+
749
+ if (!RE_INT.test (scale)) throw Error (`Not a positive integer as scale`)
750
+
751
+ o.scale = parseInt (scale)
752
+
753
+ dimension = dimension.slice (0, pos).trim ()
754
+
755
+ }
756
+
757
+ if (!RE_INT.test (dimension)) throw Error (`Not a positive integer as column dimension`)
758
+
759
+ o.size = parseInt (dimension)
760
+
761
+ }
762
+
551
763
  }
552
764
 
553
765
  module.exports = DbLang
@@ -1,175 +1,23 @@
1
1
  const DbReference = require ('./DbReference.js')
2
2
 
3
- const CH_ROUND_OPEN = '('.charCodeAt (0)
4
- const CH_ROUND_CLOSE = ')'.charCodeAt (0)
5
- const CH_CURLY_CLOSE = '}'.charCodeAt (0)
6
- const CH_SLASH = '/'.charCodeAt (0)
7
- const CH_BACK_SLASH = '\\'.charCodeAt (0)
8
- const CH_QUESTION = '?'.charCodeAt (0)
9
- const CH_EXCLAMATION = '!'.charCodeAt (0)
10
-
11
- const RE_INT = /^[1-9][0-9]*$/
12
-
13
3
  class DbColumn {
14
4
 
15
5
  constructor (o) {
16
6
 
17
- if (typeof o === 'string') {
18
-
19
- this.src = o
20
-
21
- this.parse ()
22
-
23
- }
24
- else {
25
-
26
- for (const k in o) this [k] = o [k]
27
-
28
- if (!('nullable' in this)) this.nullable = !('default' in this)
29
-
30
- }
31
-
32
- }
33
-
34
- parse () {
35
-
36
- const {src} = this, pos = src.lastIndexOf ('//')
37
-
38
- if (pos < 0) {
39
-
40
- this.type = src.trim ()
41
-
42
- }
43
- else {
44
-
45
- this.type = src.slice (0, pos).trim ()
7
+ const {isReference} = o
46
8
 
47
- this.comment = src.slice (pos + 2).trim ()
9
+ for (const k in o) if (k !== 'isReference') this [k] = o [k]
48
10
 
49
- }
50
-
51
- this.parseDefault ()
52
- this.parsePattern ()
53
- this.parseRange ()
54
- this.parseNullable ()
55
-
56
- if (this.type.charCodeAt (0) === CH_ROUND_OPEN) {
11
+ if (isReference) {
57
12
 
58
13
  this.reference = new DbReference (this.type, this)
59
-
14
+
60
15
  this.type = undefined
61
-
62
- }
63
- else {
64
-
65
- this.parseDimension ()
66
-
67
- }
68
16
 
69
- }
17
+ }
70
18
 
71
- parseDefault () {
72
-
73
- const pos = this.type.indexOf ('='); if (pos < 0) return
74
-
75
- this.default = this.type.slice (pos + 1).trim ()
76
-
77
- this.type = this.type.slice (0, pos).trim ()
78
-
79
- }
80
-
81
- parsePattern () {
82
-
83
- const last = this.default ? 'default' : 'type', src = this [last]
84
-
85
- let pos = src.length - 1; if (src.charCodeAt (pos) !== CH_SLASH) return
86
-
87
- while (true) {
88
-
89
- pos = src.lastIndexOf ('/', pos - 1)
90
-
91
- if (pos < 0) throw Error (`Invalid column definition: cannot find opening '/' for pattern in '${this.src}'`)
92
-
93
- if (pos === 0) throw Error (`Invalid column definition: the ${last} is empty in '${this.src}'`)
94
-
95
- if (src.charCodeAt (pos - 1) !== CH_BACK_SLASH) break
19
+ if (!('nullable' in this)) this.nullable = !('default' in this)
96
20
 
97
- }
98
-
99
- this.pattern = src.slice (pos + 1, -1)
100
-
101
- this [last] = src.slice (0, pos).trim ()
102
-
103
- }
104
-
105
- parseRange () {
106
-
107
- const last = this.default ? 'default' : 'type', src = this [last]
108
-
109
- if (src.charCodeAt (src.length - 1) !== CH_CURLY_CLOSE) return
110
-
111
- const begin = src.lastIndexOf ('{'); if (begin < 0) throw Error (`Invalid column definition: cannot find opening '{' for range in '${this.src}'`)
112
-
113
- const range = src.slice (begin + 1, -1)
114
-
115
- this [last] = src.slice (0, begin).trim ()
116
-
117
- const pos = range.indexOf ('..'); if (pos < 0) throw Error (`Invalid column definition: cannot find '..' between '{' and '}' in '${this.src}'`)
118
-
119
- const min = range.slice (0, pos).trim (); if (min.length !== 0) this.min = min
120
-
121
- const max = range.slice (pos + 2).trim (); if (max.length !== 0) this.max = max
122
-
123
- }
124
-
125
- parseNullable () {
126
-
127
- let nullable = !('default' in this)
128
-
129
- const src = this.type, {length} = src; if (length === 0) throw Error (`Invalid column definition: cannot determine type in '${this.src}'`)
130
-
131
- const override = nullable ? CH_EXCLAMATION : CH_QUESTION
132
-
133
- if (src.charCodeAt (length - 1) === override) {
134
-
135
- nullable = !nullable
136
-
137
- this.type = src.slice (0, -1).trim ()
138
-
139
- }
140
-
141
- this.nullable = nullable
142
-
143
- }
144
-
145
- parseDimension () {
146
-
147
- const src = this.type
148
-
149
- if (src.charCodeAt (src.length - 1) !== CH_ROUND_CLOSE) return
150
-
151
- const begin = src.lastIndexOf ('('); if (begin < 0) throw Error (`Invalid column definition: cannot find opening '(' for dimension in '${this.src}'`)
152
-
153
- let dimension = src.slice (begin + 1, -1)
154
-
155
- this.type = src.slice (0, begin).trim ()
156
-
157
- const pos = dimension.indexOf (','); if (pos >= 0) {
158
-
159
- const scale = dimension.slice (pos + 1).trim ()
160
-
161
- if (!RE_INT.test (scale)) throw Error (`Invalid column definition: not a positive integer as scale in '${this.src}'`)
162
-
163
- this.scale = parseInt (scale)
164
-
165
- dimension = dimension.slice (0, pos).trim ()
166
-
167
- }
168
-
169
- if (!RE_INT.test (dimension)) throw Error (`Invalid column definition: not a positive integer as column dimension in '${this.src}'`)
170
-
171
- this.size = parseInt (dimension)
172
-
173
21
  }
174
22
 
175
23
  setLang (lang) {
@@ -42,14 +42,12 @@ class DbSchema extends require ('events') {
42
42
 
43
43
  create (options) {
44
44
 
45
- const {model} = this, {lang} = model
45
+ const {model} = this, {lang} = model
46
46
 
47
47
  options.model = model
48
48
  options.schema = this
49
49
 
50
- const dbObject = new (lang.getDbObjectClass (options)) (options)
51
-
52
- dbObject.setLang (lang)
50
+ const dbObject = lang.createDbObject (options)
53
51
 
54
52
  this.emit ('object-created', dbObject)
55
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doix-db",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "Shared database related code for doix",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "homepage": "https://github.com/do-/node-doix-db#readme",
42
42
  "peerDependencies": {
43
- "doix": "^1.0.51"
43
+ "doix": "^1.0.52"
44
44
  },
45
45
  "devDependencies": {
46
46
  "jest": "^29.6.1"