meadow 2.0.27 → 2.0.29

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/.quackage.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "GulpExecutions": [
3
+ {
4
+ "Hash": "default",
5
+ "Name": "Default standard build.",
6
+ "BuildFileLabel": "",
7
+ "BrowsersListRC": "since 2022"
8
+ }]
9
+ }
package/README.md CHANGED
@@ -41,6 +41,7 @@ const meadow = libMeadow.new(libFable, 'Book')
41
41
  { Column: 'GUIDBook', Type: 'AutoGUID' },
42
42
  { Column: 'Title', Type: 'String', Size: '255' },
43
43
  { Column: 'Author', Type: 'String', Size: '128' },
44
+ { Column: 'Metadata', Type: 'JSON' },
44
45
  { Column: 'CreateDate', Type: 'CreateDate' },
45
46
  { Column: 'CreatingIDUser', Type: 'CreateIDUser' },
46
47
  { Column: 'UpdateDate', Type: 'UpdateDate' },
@@ -92,6 +93,8 @@ meadow.doReads(meadow.query.setCap(25).setBegin(0),
92
93
  | `DeleteDate` | Auto-populated timestamp on delete |
93
94
  | `DeleteIDUser` | Auto-populated user ID on delete |
94
95
  | `Deleted` | Soft delete flag (enables logical deletion) |
96
+ | `JSON` | Structured JSON data (stored as `TEXT`, marshaled to/from objects) |
97
+ | `JSONProxy` | JSON stored in a different SQL column (uses `StorageColumn` for SQL, virtual name for objects) |
95
98
 
96
99
  ## CRUD Operations
97
100
 
package/docs/_sidebar.md CHANGED
@@ -8,6 +8,7 @@
8
8
  - Core Concepts
9
9
 
10
10
  - [Schema](schema/README.md)
11
+ - [JSON Columns](schema/json-columns.md)
11
12
  - [Query DSL](query-dsl.md)
12
13
  - [Raw Queries](raw-queries.md)
13
14
  - [Audit Tracking](audit-tracking.md)
@@ -34,6 +34,8 @@ const tmpSchema =
34
34
  { Column: 'InPrint', Type: 'Boolean' },
35
35
  { Column: 'PublishDate', Type: 'DateTime' },
36
36
  { Column: 'Synopsis', Type: 'Text' },
37
+ { Column: 'Metadata', Type: 'JSON' },
38
+ { Column: 'Extras', Type: 'JSONProxy', StorageColumn: 'ExtrasJSON' },
37
39
  { Column: 'CreateDate', Type: 'CreateDate' },
38
40
  { Column: 'CreatingIDUser', Type: 'CreateIDUser' },
39
41
  { Column: 'UpdateDate', Type: 'UpdateDate' },
@@ -59,6 +61,40 @@ meadow.setSchema(tmpSchema);
59
61
  | `Decimal` | Decimal number | `DECIMAL(precision,scale)` | `0` |
60
62
  | `Boolean` | Boolean flag | `INT` / `BOOLEAN` | `0` |
61
63
  | `DateTime` | Date/time field | `DATETIME` | `null` |
64
+ | `JSON` | Structured JSON data | `LONGTEXT` (MySQL) / `TEXT` (others) | `{}` |
65
+ | `JSONProxy` | JSON with different SQL column name | `LONGTEXT` / `TEXT` (on `StorageColumn`) | `{}` |
66
+
67
+ ### JSON Column Types
68
+
69
+ The `JSON` and `JSONProxy` types store structured data as serialized JSON text in SQL databases. Meadow handles serialization and deserialization automatically in the provider layer.
70
+
71
+ **JSON** -- the SQL column and object property share the same name:
72
+
73
+ ```javascript
74
+ { Column: 'Metadata', Type: 'JSON' }
75
+ ```
76
+
77
+ On create/update, the object value is `JSON.stringify`'d into the `Metadata` TEXT column. On read, the text is `JSON.parse`'d back into an object on `record.Metadata`.
78
+
79
+ **JSONProxy** -- the SQL column name differs from the JavaScript property:
80
+
81
+ ```javascript
82
+ { Column: 'Preferences', Type: 'JSONProxy', StorageColumn: 'PreferencesJSON' }
83
+ ```
84
+
85
+ On create/update, the value from `record.Preferences` is `JSON.stringify`'d and stored in the `PreferencesJSON` SQL column. On read, `PreferencesJSON` is parsed and mapped to `record.Preferences`. The storage column (`PreferencesJSON`) is never exposed on the marshaled record object.
86
+
87
+ This separation is useful when you want a clean API surface (`record.Preferences`) while keeping a database naming convention that makes the storage format explicit (`PreferencesJSON`).
88
+
89
+ For object-store providers (MongoDB, RocksDB), JSON values are stored and retrieved as native objects -- no serialization is needed.
90
+
91
+ JSON columns support the standard `Column` and `Type` properties, plus:
92
+
93
+ | Property | Required | Description |
94
+ |----------|----------|-------------|
95
+ | `StorageColumn` | JSONProxy only | The actual SQL column name for storage |
96
+
97
+ See [JSON Columns](json-columns.md) for detailed examples and filtering.
62
98
 
63
99
  ### Audit Column Types
64
100
 
@@ -0,0 +1,188 @@
1
+ # JSON Columns
2
+
3
+ > Store structured data as JSON in SQL databases with automatic serialization
4
+
5
+ Meadow supports two column types for storing structured JSON data: `JSON` and `JSONProxy`. Both store data as `TEXT` in SQL databases and handle serialization/deserialization automatically. Object-store providers (MongoDB, RocksDB) store these values as native objects.
6
+
7
+ ## JSON Type
8
+
9
+ The `JSON` type stores structured data in a column where the SQL name and JavaScript property name are identical.
10
+
11
+ ### Schema Definition
12
+
13
+ ```javascript
14
+ { Column: 'Metadata', Type: 'JSON' }
15
+ ```
16
+
17
+ ### MicroDDL Syntax
18
+
19
+ ```
20
+ {Metadata
21
+ ```
22
+
23
+ ### Behavior
24
+
25
+ | Operation | What Happens |
26
+ |-----------|-------------|
27
+ | **Create** | `record.Metadata` is `JSON.stringify`'d into the `Metadata` TEXT column |
28
+ | **Read** | The `Metadata` TEXT column is `JSON.parse`'d into `record.Metadata` (an object) |
29
+ | **Update** | Same as Create -- the value is serialized before writing |
30
+ | **Default** | `{}` (empty object) |
31
+
32
+ ### Example
33
+
34
+ ```javascript
35
+ // Schema
36
+ const schema = [
37
+ { Column: 'IDProduct', Type: 'AutoIdentity' },
38
+ { Column: 'Name', Type: 'String', Size: '128' },
39
+ { Column: 'Metadata', Type: 'JSON' },
40
+ // ... audit columns
41
+ ];
42
+
43
+ // Create with JSON data
44
+ meadow.doCreate(
45
+ meadow.query.addRecord({
46
+ Name: 'Widget',
47
+ Metadata: { color: 'blue', weight: 1.5, tags: ['sale', 'new'] }
48
+ }),
49
+ (pError, pCreateQuery, pReadQuery, pRecord) =>
50
+ {
51
+ console.log(pRecord.Metadata);
52
+ // => { color: 'blue', weight: 1.5, tags: ['sale', 'new'] }
53
+ console.log(typeof pRecord.Metadata);
54
+ // => 'object'
55
+ });
56
+ ```
57
+
58
+ ## JSONProxy Type
59
+
60
+ The `JSONProxy` type stores JSON in a SQL column with a different name than the JavaScript property. This is useful when you want a clean API surface while keeping an explicit naming convention in the database.
61
+
62
+ ### Schema Definition
63
+
64
+ ```javascript
65
+ { Column: 'Preferences', Type: 'JSONProxy', StorageColumn: 'PreferencesJSON' }
66
+ ```
67
+
68
+ | Property | Description |
69
+ |----------|-------------|
70
+ | `Column` | The JavaScript property name (virtual) -- what API consumers see |
71
+ | `StorageColumn` | The actual SQL column name -- what the database stores |
72
+
73
+ ### MicroDDL Syntax
74
+
75
+ ```
76
+ {Preferences PreferencesJSON
77
+ ```
78
+
79
+ ### Behavior
80
+
81
+ | Operation | What Happens |
82
+ |-----------|-------------|
83
+ | **Create** | `record.Preferences` is `JSON.stringify`'d into the `PreferencesJSON` SQL column |
84
+ | **Read** | The `PreferencesJSON` column is `JSON.parse`'d and mapped to `record.Preferences`; `PreferencesJSON` is **not** exposed on the result |
85
+ | **Update** | Same as Create -- serialized to the storage column |
86
+ | **Default** | `{}` (empty object) |
87
+
88
+ ### Example
89
+
90
+ ```javascript
91
+ // Schema
92
+ const schema = [
93
+ { Column: 'IDUser', Type: 'AutoIdentity' },
94
+ { Column: 'Name', Type: 'String', Size: '128' },
95
+ { Column: 'Preferences', Type: 'JSONProxy', StorageColumn: 'PreferencesJSON' },
96
+ // ... audit columns
97
+ ];
98
+
99
+ // Create
100
+ meadow.doCreate(
101
+ meadow.query.addRecord({
102
+ Name: 'Alice',
103
+ Preferences: { theme: 'dark', language: 'en', notifications: true }
104
+ }),
105
+ (pError, pCreateQuery, pReadQuery, pRecord) =>
106
+ {
107
+ console.log(pRecord.Preferences);
108
+ // => { theme: 'dark', language: 'en', notifications: true }
109
+
110
+ console.log(pRecord.PreferencesJSON);
111
+ // => undefined (storage column is hidden)
112
+ });
113
+ ```
114
+
115
+ ## SQL Storage
116
+
117
+ In SQL databases, both JSON types are stored as text columns. MySQL uses `LONGTEXT` (up to 4GB) to avoid the 64KB limit of `TEXT`. Other databases use `TEXT` (unlimited in PostgreSQL and SQLite) or `NVARCHAR(MAX)` (MSSQL).
118
+
119
+ | Provider | JSON DDL | JSONProxy DDL |
120
+ |----------|----------|---------------|
121
+ | MySQL | `Metadata LONGTEXT` | `PreferencesJSON LONGTEXT` |
122
+ | PostgreSQL | `"Metadata" TEXT` | `"PreferencesJSON" TEXT` |
123
+ | SQLite | `Metadata TEXT` | `PreferencesJSON TEXT` |
124
+ | MSSQL | `[Metadata] NVARCHAR(MAX)` | `[PreferencesJSON] NVARCHAR(MAX)` |
125
+
126
+ ## Filtering on JSON Properties
127
+
128
+ FoxHound supports filtering on nested JSON properties using dot notation:
129
+
130
+ ```javascript
131
+ // Filter by a JSON property
132
+ meadow.doReads(
133
+ meadow.query.addFilter('Metadata.color', 'blue'),
134
+ (pError, pQuery, pRecords) =>
135
+ {
136
+ // Returns records where Metadata contains { color: 'blue' }
137
+ });
138
+
139
+ // Comparison operators work too
140
+ meadow.doReads(
141
+ meadow.query.addFilter('Metadata.weight', 2.0, '>='),
142
+ (pError, pQuery, pRecords) =>
143
+ {
144
+ // Returns records where Metadata.weight >= 2.0
145
+ });
146
+ ```
147
+
148
+ The generated SQL uses dialect-specific JSON path functions:
149
+
150
+ | Dialect | Generated SQL |
151
+ |---------|---------------|
152
+ | MySQL | `JSON_EXTRACT(Metadata, '$.color') = :Metadata_color_w0` |
153
+ | PostgreSQL | `Metadata->>'color' = :Metadata_color_w0` |
154
+ | SQLite | `json_extract(Metadata, '$.color') = :Metadata_color_w0` |
155
+ | MSSQL | `JSON_VALUE(Metadata, '$.color') = :Metadata_color_w0` |
156
+
157
+ Nested paths are supported (e.g., `Metadata.dimensions.width`).
158
+
159
+ For `JSONProxy` columns, the storage column is used in the SQL expression automatically. Filtering on `Preferences.theme` produces `json_extract(PreferencesJSON, '$.theme')` in SQLite.
160
+
161
+ ## Package File Format
162
+
163
+ When using a JSON package file to define your schema:
164
+
165
+ ```json
166
+ {
167
+ "Scope": "Product",
168
+ "DefaultIdentifier": "IDProduct",
169
+ "Schema": [
170
+ { "Column": "IDProduct", "Type": "AutoIdentity" },
171
+ { "Column": "Name", "Type": "String", "Size": "128" },
172
+ { "Column": "Metadata", "Type": "JSON" },
173
+ { "Column": "Preferences", "Type": "JSONProxy", "StorageColumn": "PreferencesJSON" }
174
+ ],
175
+ "DefaultObject": {
176
+ "IDProduct": null,
177
+ "Name": "",
178
+ "Metadata": {},
179
+ "Preferences": {}
180
+ }
181
+ }
182
+ ```
183
+
184
+ Note that `DefaultObject` uses the virtual property name (`Preferences`), not the storage column name.
185
+
186
+ ## Object Store Providers
187
+
188
+ MongoDB and RocksDB store JSON values as native objects -- no serialization or deserialization is needed. The `JSON` and `JSONProxy` types work transparently with these providers; the marshaling layer simply passes the objects through.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meadow",
3
- "version": "2.0.27",
3
+ "version": "2.0.29",
4
4
  "description": "A data access library.",
5
5
  "main": "source/Meadow.js",
6
6
  "scripts": {
@@ -9,6 +9,7 @@
9
9
  "test": "npx quack test",
10
10
  "tests": "npx quack test -g",
11
11
  "build": "npx quack build",
12
+ "prepublishOnly": "npm run build",
12
13
  "docker-dev-build": "docker build ./ -f Dockerfile_LUXURYCode -t retold/meadow:local",
13
14
  "docker-dev-run": "docker run -it -d --name meadow-dev -p 12342:8080 -p 12106:3306 -v \"$PWD/.config:/home/coder/.config\" -v \"$PWD:/home/coder/meadow\" -u \"$(id -u):$(id -g)\" -e \"DOCKER_USER=$USER\" retold/meadow:local",
14
15
  "docker-dev-shell": "docker exec -it meadow-dev /bin/bash",
@@ -86,22 +87,22 @@
86
87
  "gulp-util": "^3.0.8",
87
88
  "meadow-connection-dgraph": "^1.0.2",
88
89
  "meadow-connection-mongodb": "^1.0.2",
89
- "meadow-connection-mssql": "^1.0.15",
90
- "meadow-connection-mysql": "^1.0.13",
91
- "meadow-connection-postgresql": "^1.0.1",
90
+ "meadow-connection-mssql": "^1.0.16",
91
+ "meadow-connection-mysql": "^1.0.14",
92
+ "meadow-connection-postgresql": "^1.0.2",
92
93
  "meadow-connection-rocksdb": "^0.0.2",
93
94
  "meadow-connection-solr": "^1.0.2",
94
- "meadow-connection-sqlite": "^1.0.17",
95
- "meadow-connection-sqlite-browser": "^1.0.0",
95
+ "meadow-connection-sqlite": "^1.0.18",
96
+ "meadow-connection-sqlite-browser": "^1.0.1",
96
97
  "mongodb": "^6.12.0",
97
98
  "mysql2": "^3.18.2",
98
99
  "puppeteer": "^24.38.0",
99
- "quackage": "^1.0.60",
100
+ "quackage": "^1.0.63",
100
101
  "solr-client": "^0.9.0"
101
102
  },
102
103
  "dependencies": {
103
104
  "async": "3.2.6",
104
- "foxhound": "^2.0.22",
105
+ "foxhound": "^2.0.25",
105
106
  "is-my-json-valid": "2.20.6",
106
107
  "simple-get": "^4.0.1"
107
108
  }
@@ -92,6 +92,12 @@ var MeadowProvider = function()
92
92
  case 'Text':
93
93
  tmpCreateStatement += " `"+tmpSchema[j].Column+"` TEXT";
94
94
  break;
95
+ case 'JSON':
96
+ tmpCreateStatement += " `"+tmpSchema[j].Column+"` TEXT";
97
+ break;
98
+ case 'JSONProxy':
99
+ tmpCreateStatement += " `"+tmpSchema[j].StorageColumn+"` TEXT";
100
+ break;
95
101
  case 'CreateDate':
96
102
  case 'UpdateDate':
97
103
  case 'DeleteDate':
@@ -145,14 +151,63 @@ var MeadowProvider = function()
145
151
  return true;
146
152
  };
147
153
 
148
- // The Meadow marshaller also passes in the Schema as the third parameter, but this is a blunt function ATM.
149
- var marshalRecordFromSourceToObject = function(pObject, pRecord)
154
+ // The Meadow marshaller passes in the Schema as the third parameter for JSON/JSONProxy deserialization.
155
+ var marshalRecordFromSourceToObject = function (pObject, pRecord, pSchema)
150
156
  {
151
- // For now, crudely assign everything in pRecord to pObject
152
- // This is safe in this context, and we don't want to slow down marshalling with millions of hasOwnProperty checks
153
- for(var tmpColumn in pRecord)
157
+ // Build lookups for JSON columns (only if schema is provided)
158
+ var tmpJsonColumns = {};
159
+ var tmpProxyColumns = {};
160
+ if (Array.isArray(pSchema))
154
161
  {
155
- pObject[tmpColumn] = pRecord[tmpColumn];
162
+ for (var s = 0; s < pSchema.length; s++)
163
+ {
164
+ if (pSchema[s].Type === 'JSON')
165
+ {
166
+ tmpJsonColumns[pSchema[s].Column] = true;
167
+ }
168
+ else if (pSchema[s].Type === 'JSONProxy' && pSchema[s].StorageColumn)
169
+ {
170
+ tmpProxyColumns[pSchema[s].StorageColumn] = pSchema[s].Column;
171
+ }
172
+ }
173
+ }
174
+
175
+ for (var tmpColumn in pRecord)
176
+ {
177
+ if (tmpJsonColumns[tmpColumn])
178
+ {
179
+ // JSON type: parse string from DB into object on the same column name
180
+ try
181
+ {
182
+ pObject[tmpColumn] = (typeof pRecord[tmpColumn] === 'string')
183
+ ? JSON.parse(pRecord[tmpColumn])
184
+ : (pRecord[tmpColumn] || {});
185
+ }
186
+ catch (pParseError)
187
+ {
188
+ pObject[tmpColumn] = {};
189
+ }
190
+ }
191
+ else if (tmpProxyColumns.hasOwnProperty(tmpColumn))
192
+ {
193
+ // JSONProxy: storage column -> parse and assign to virtual column name
194
+ var tmpVirtualColumn = tmpProxyColumns[tmpColumn];
195
+ try
196
+ {
197
+ pObject[tmpVirtualColumn] = (typeof pRecord[tmpColumn] === 'string')
198
+ ? JSON.parse(pRecord[tmpColumn])
199
+ : (pRecord[tmpColumn] || {});
200
+ }
201
+ catch (pParseError)
202
+ {
203
+ pObject[tmpVirtualColumn] = {};
204
+ }
205
+ // Do NOT copy the storage column to the output object
206
+ }
207
+ else
208
+ {
209
+ pObject[tmpColumn] = pRecord[tmpColumn];
210
+ }
156
211
  }
157
212
  };
158
213
 
@@ -43,14 +43,63 @@ var MeadowProvider = function ()
43
43
  return false;
44
44
  }
45
45
 
46
- // The Meadow marshaller also passes in the Schema as the third parameter, but this is a blunt function ATM.
47
- var marshalRecordFromSourceToObject = function (pObject, pRecord)
46
+ // The Meadow marshaller passes in the Schema as the third parameter for JSON/JSONProxy deserialization.
47
+ var marshalRecordFromSourceToObject = function (pObject, pRecord, pSchema)
48
48
  {
49
- // For now, crudely assign everything in pRecord to pObject
50
- // This is safe in this context, and we don't want to slow down marshalling with millions of hasOwnProperty checks
49
+ // Build lookups for JSON columns (only if schema is provided)
50
+ var tmpJsonColumns = {};
51
+ var tmpProxyColumns = {};
52
+ if (Array.isArray(pSchema))
53
+ {
54
+ for (var s = 0; s < pSchema.length; s++)
55
+ {
56
+ if (pSchema[s].Type === 'JSON')
57
+ {
58
+ tmpJsonColumns[pSchema[s].Column] = true;
59
+ }
60
+ else if (pSchema[s].Type === 'JSONProxy' && pSchema[s].StorageColumn)
61
+ {
62
+ tmpProxyColumns[pSchema[s].StorageColumn] = pSchema[s].Column;
63
+ }
64
+ }
65
+ }
66
+
51
67
  for (var tmpColumn in pRecord)
52
68
  {
53
- pObject[tmpColumn] = pRecord[tmpColumn];
69
+ if (tmpJsonColumns[tmpColumn])
70
+ {
71
+ // JSON type: parse string from DB into object on the same column name
72
+ try
73
+ {
74
+ pObject[tmpColumn] = (typeof pRecord[tmpColumn] === 'string')
75
+ ? JSON.parse(pRecord[tmpColumn])
76
+ : (pRecord[tmpColumn] || {});
77
+ }
78
+ catch (pParseError)
79
+ {
80
+ pObject[tmpColumn] = {};
81
+ }
82
+ }
83
+ else if (tmpProxyColumns.hasOwnProperty(tmpColumn))
84
+ {
85
+ // JSONProxy: storage column -> parse and assign to virtual column name
86
+ var tmpVirtualColumn = tmpProxyColumns[tmpColumn];
87
+ try
88
+ {
89
+ pObject[tmpVirtualColumn] = (typeof pRecord[tmpColumn] === 'string')
90
+ ? JSON.parse(pRecord[tmpColumn])
91
+ : (pRecord[tmpColumn] || {});
92
+ }
93
+ catch (pParseError)
94
+ {
95
+ pObject[tmpVirtualColumn] = {};
96
+ }
97
+ // Do NOT copy the storage column to the output object
98
+ }
99
+ else
100
+ {
101
+ pObject[tmpColumn] = pRecord[tmpColumn];
102
+ }
54
103
  }
55
104
  };
56
105
 
@@ -116,13 +116,25 @@ var MeadowProvider = function()
116
116
  pResponse.on('end', ()=>
117
117
  {
118
118
  if (tmpData)
119
- tmpResult.value = JSON.parse(tmpData);
119
+ {
120
+ try
121
+ {
122
+ tmpResult.value = JSON.parse(tmpData);
123
+ }
124
+ catch (pParseError)
125
+ {
126
+ tmpResult.error = new Error(`Failed to parse Create response as JSON: ${pParseError.message}`);
127
+ return fCallback();
128
+ }
129
+ }
120
130
 
121
131
  // TODO Because this was proxied, read happens at this layer too. Inefficient -- fixable
122
- let tmpIdentityColumn = `ID${pQuery.parameters.scope}`;
123
- if (tmpResult.value.hasOwnProperty(tmpIdentityColumn))
124
- tmpResult.value = tmpResult.value[tmpIdentityColumn];
125
-
132
+ const tmpIdentityColumn = `ID${pQuery.parameters.scope}`;
133
+ if (tmpResult.value && tmpResult.value.hasOwnProperty(tmpIdentityColumn))
134
+ {
135
+ tmpResult.value = tmpResult.value[tmpIdentityColumn];
136
+ }
137
+
126
138
  if (pQuery.logLevel > 0 ||
127
139
  _GlobalLogLevel > 0)
128
140
  {
@@ -169,7 +181,17 @@ var MeadowProvider = function()
169
181
  pResponse.on('end', ()=>
170
182
  {
171
183
  if (tmpData)
172
- tmpResult.value = JSON.parse(tmpData);
184
+ {
185
+ try
186
+ {
187
+ tmpResult.value = JSON.parse(tmpData);
188
+ }
189
+ catch (pParseError)
190
+ {
191
+ tmpResult.error = new Error(`Failed to parse Read response as JSON: ${pParseError.message}`);
192
+ return fCallback();
193
+ }
194
+ }
173
195
 
174
196
  if (pQuery.query.body.startsWith(`${pQuery.parameters.scope}/`))
175
197
  {
@@ -232,13 +254,23 @@ var MeadowProvider = function()
232
254
  pResponse.on('end', ()=>
233
255
  {
234
256
  if (tmpData)
235
- tmpResult.value = JSON.parse(tmpData);
257
+ {
258
+ try
259
+ {
260
+ tmpResult.value = JSON.parse(tmpData);
261
+ }
262
+ catch (pParseError)
263
+ {
264
+ tmpResult.error = new Error(`Failed to parse Update response as JSON: ${pParseError.message}`);
265
+ return fCallback();
266
+ }
267
+ }
268
+
269
+ // Keep result.value as the full response object so the
270
+ // Meadow Update waterfall's typeof check passes (it expects
271
+ // an object). The subsequent Read step uses the existing
272
+ // filters to re-read the updated record.
236
273
 
237
- // TODO Because this was proxied, read happens at this layer too. Inefficient -- fixable
238
- let tmpIdentityColumn = `ID${pQuery.parameters.scope}`;
239
- if (tmpResult.value.hasOwnProperty(tmpIdentityColumn))
240
- tmpResult.value = tmpResult.value[tmpIdentityColumn];
241
-
242
274
  if (pQuery.logLevel > 0 ||
243
275
  _GlobalLogLevel > 0)
244
276
  {
@@ -284,11 +316,22 @@ var MeadowProvider = function()
284
316
  pResponse.on('end', ()=>
285
317
  {
286
318
  if (tmpData)
287
- tmpResult.value = JSON.parse(tmpData);
288
-
289
- if (tmpResult.value.hasOwnProperty('Count'))
319
+ {
320
+ try
321
+ {
322
+ tmpResult.value = JSON.parse(tmpData);
323
+ }
324
+ catch (pParseError)
325
+ {
326
+ tmpResult.error = new Error(`Failed to parse Delete response as JSON: ${pParseError.message}`);
327
+ return fCallback();
328
+ }
329
+ }
330
+
331
+ if (tmpResult.value && tmpResult.value.hasOwnProperty('Count'))
332
+ {
290
333
  tmpResult.value = tmpResult.value.Count;
291
-
334
+ }
292
335
 
293
336
  if (pQuery.logLevel > 0 ||
294
337
  _GlobalLogLevel > 0)
@@ -334,19 +377,29 @@ var MeadowProvider = function()
334
377
  pResponse.on('end', ()=>
335
378
  {
336
379
  if (tmpData)
337
- tmpResult.value = JSON.parse(tmpData);
338
-
380
+ {
339
381
  try
340
382
  {
341
- tmpResult.value = tmpResult.value.Count;
383
+ tmpResult.value = JSON.parse(tmpData);
342
384
  }
343
- catch(pErrorGettingRowcount)
385
+ catch (pParseError)
344
386
  {
345
- // This is an error state...
346
- tmpResult.value = -1;
347
- _Fable.log.warn('Error getting rowcount during count query',{Body:pQuery.query.body, Parameters:pQuery.query.parameters});
387
+ tmpResult.error = new Error(`Failed to parse Count response as JSON: ${pParseError.message}`);
388
+ return fCallback();
348
389
  }
349
-
390
+ }
391
+
392
+ try
393
+ {
394
+ tmpResult.value = tmpResult.value.Count;
395
+ }
396
+ catch(pErrorGettingRowcount)
397
+ {
398
+ // This is an error state...
399
+ tmpResult.value = -1;
400
+ _Fable.log.warn('Error getting rowcount during count query',{Body:pQuery.query.body, Parameters:pQuery.query.parameters});
401
+ }
402
+
350
403
  if (pQuery.logLevel > 0 ||
351
404
  _GlobalLogLevel > 0)
352
405
  {
@@ -57,14 +57,63 @@ var MeadowProvider = function ()
57
57
  return false;
58
58
  }
59
59
 
60
- // The Meadow marshaller also passes in the Schema as the third parameter, but this is a blunt function ATM.
61
- var marshalRecordFromSourceToObject = function (pObject, pRecord)
60
+ // The Meadow marshaller passes in the Schema as the third parameter for JSON/JSONProxy deserialization.
61
+ var marshalRecordFromSourceToObject = function (pObject, pRecord, pSchema)
62
62
  {
63
- // For now, crudely assign everything in pRecord to pObject
64
- // This is safe in this context, and we don't want to slow down marshalling with millions of hasOwnProperty checks
63
+ // Build lookups for JSON columns (only if schema is provided)
64
+ var tmpJsonColumns = {};
65
+ var tmpProxyColumns = {};
66
+ if (Array.isArray(pSchema))
67
+ {
68
+ for (var s = 0; s < pSchema.length; s++)
69
+ {
70
+ if (pSchema[s].Type === 'JSON')
71
+ {
72
+ tmpJsonColumns[pSchema[s].Column] = true;
73
+ }
74
+ else if (pSchema[s].Type === 'JSONProxy' && pSchema[s].StorageColumn)
75
+ {
76
+ tmpProxyColumns[pSchema[s].StorageColumn] = pSchema[s].Column;
77
+ }
78
+ }
79
+ }
80
+
65
81
  for (var tmpColumn in pRecord)
66
82
  {
67
- pObject[tmpColumn] = pRecord[tmpColumn];
83
+ if (tmpJsonColumns[tmpColumn])
84
+ {
85
+ // JSON type: parse string from DB into object on the same column name
86
+ try
87
+ {
88
+ pObject[tmpColumn] = (typeof pRecord[tmpColumn] === 'string')
89
+ ? JSON.parse(pRecord[tmpColumn])
90
+ : (pRecord[tmpColumn] || {});
91
+ }
92
+ catch (pParseError)
93
+ {
94
+ pObject[tmpColumn] = {};
95
+ }
96
+ }
97
+ else if (tmpProxyColumns.hasOwnProperty(tmpColumn))
98
+ {
99
+ // JSONProxy: storage column -> parse and assign to virtual column name
100
+ var tmpVirtualColumn = tmpProxyColumns[tmpColumn];
101
+ try
102
+ {
103
+ pObject[tmpVirtualColumn] = (typeof pRecord[tmpColumn] === 'string')
104
+ ? JSON.parse(pRecord[tmpColumn])
105
+ : (pRecord[tmpColumn] || {});
106
+ }
107
+ catch (pParseError)
108
+ {
109
+ pObject[tmpVirtualColumn] = {};
110
+ }
111
+ // Do NOT copy the storage column to the output object
112
+ }
113
+ else
114
+ {
115
+ pObject[tmpColumn] = pRecord[tmpColumn];
116
+ }
68
117
  }
69
118
  };
70
119