@flourish/sdk 3.16.0 → 3.17.0

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/RELEASE_NOTES.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 3.17.0
2
+
3
+ * Allow published visualisations to be cloned into the SDK
4
+
1
5
  # 3.16.0
2
6
  * Upgrade all NPM dependencies.
3
7
 
@@ -0,0 +1,36 @@
1
+ {
2
+ "decisions": {
3
+ "1002401|npm-audit-resolver>yargs-unparser>yargs>string-width>strip-ansi>ansi-regex": {
4
+ "decision": "ignore",
5
+ "madeAt": 1637059117447,
6
+ "expiresAt": 1637663913365
7
+ },
8
+ "1002401|npm-audit-resolver>yargs-unparser>yargs>cliui>string-width>strip-ansi>ansi-regex": {
9
+ "decision": "ignore",
10
+ "madeAt": 1637059117447,
11
+ "expiresAt": 1637663913365
12
+ },
13
+ "1002401|npm-audit-resolver>yargs-unparser>yargs>cliui>wrap-ansi>string-width>strip-ansi>ansi-regex": {
14
+ "decision": "ignore",
15
+ "madeAt": 1637059117447,
16
+ "expiresAt": 1637663913365
17
+ },
18
+ "1004946|npm-audit-resolver>yargs-unparser>yargs>string-width>strip-ansi>ansi-regex": {
19
+ "decision": "ignore",
20
+ "madeAt": 1639560165214,
21
+ "expiresAt": 1642152161687
22
+ },
23
+ "1004946|npm-audit-resolver>yargs-unparser>yargs>cliui>string-width>strip-ansi>ansi-regex": {
24
+ "decision": "ignore",
25
+ "madeAt": 1639560165214,
26
+ "expiresAt": 1642152161687
27
+ },
28
+ "1004946|npm-audit-resolver>yargs-unparser>yargs>cliui>wrap-ansi>string-width>strip-ansi>ansi-regex": {
29
+ "decision": "ignore",
30
+ "madeAt": 1639560165214,
31
+ "expiresAt": 1642152161687
32
+ }
33
+ },
34
+ "rules": {},
35
+ "version": 1
36
+ }
@@ -9,7 +9,7 @@ function assign_version_number(args, server_opts) {
9
9
  let template_id_promise, template_version;
10
10
  if (args._.length == 2) {
11
11
  // Assume the supplied argument is a version number, and try to get the id from the current directory
12
- template_id_promise = sdk.readAndValidateConfig(".").then(config => config.id);
12
+ template_id_promise = sdk.readAndValidateConfig(".").then(({config}) => config.id);
13
13
  template_version = args._[1];
14
14
  }
15
15
  else if (args._.length == 3) {
package/lib/cmd/help.js CHANGED
@@ -24,7 +24,7 @@ help.help = `
24
24
  Commands:
25
25
  flourish assign-version-number [template id] version
26
26
  flourish build [build rules...]
27
- flourish delete [--force] template_id
27
+ flourish delete [--force] template_id version
28
28
  flourish [-h|--help|help] [topic]
29
29
  flourish history [--full] template_id
30
30
  flourish list [--full] [template id]
@@ -5,9 +5,13 @@ var fs = require("fs"),
5
5
 
6
6
  archiver = require("archiver"),
7
7
  tmp = require("tmp"),
8
+ FormData = require("form-data"),
9
+
10
+ d3_dsv = require("d3-dsv"),
8
11
 
9
12
  log = require("../log"),
10
- sdk = require("../sdk");
13
+ sdk = require("../sdk"),
14
+ data_utils = require("../../server/data");
11
15
 
12
16
  function zipUpTemplate(template_dir, config) {
13
17
  return new Promise(function(resolve, reject) {
@@ -48,6 +52,21 @@ function zipUpTemplate(template_dir, config) {
48
52
  if (config.settings) zip.append(JSON.stringify(config.settings), { name: "settings.js" });
49
53
  if (config.data) zip.append(JSON.stringify(config.data), { name: "data.json" });
50
54
 
55
+ const data_dir = path.join(template_dir, "data");
56
+ if (fs.existsSync(data_dir)) {
57
+ // FIXME: check inferred types are compatible with specified types of data bindings
58
+ const column_types_by_sheet = {};
59
+ const files = fs.readdirSync(data_dir).filter(d => d.endsWith(".csv"));
60
+ for (const file of files) {
61
+ let csv_text = fs.readFileSync(path.join(template_dir, "data", file), "utf8");
62
+ if (csv_text.charAt(0) === "\uFEFF") csv_text = csv_text.substr(1);
63
+ const parsed_csv = d3_dsv.csvParseRows(csv_text);
64
+ column_types_by_sheet[file.replace(/\.csv$/, "")] = data_utils.getColumnTypesForData(parsed_csv);
65
+ }
66
+ zip.append(JSON.stringify(column_types_by_sheet), { name: "column_types_by_sheet.json" });
67
+ }
68
+
69
+
51
70
  if (fs.existsSync(path.join(template_dir, "GUIDE.md"))) {
52
71
  let file_path = path.join(template_dir, "GUIDE.md");
53
72
  zip.file(file_path, { name: "README.md" });
@@ -77,17 +96,16 @@ function zipUpTemplate(template_dir, config) {
77
96
  }
78
97
 
79
98
  function uploadTemplate(server_opts, template_id, external_version, zip_filename) {
80
- return sdk.request(server_opts, "template/publish", {
81
- id: template_id,
82
- version: external_version,
83
- template: {
84
- value: fs.createReadStream(zip_filename),
85
- options: {
86
- filename: "template.zip",
87
- contentType: "application/zip",
88
- }
89
- }
99
+ const body = new FormData();
100
+
101
+ body.append("id", template_id);
102
+ body.append("version", external_version);
103
+ body.append("template", fs.createReadStream(zip_filename), {
104
+ contentType: "application/zip",
105
+ filename: "template.zip"
90
106
  });
107
+
108
+ return sdk.request(server_opts, "template/publish", body);
91
109
  }
92
110
 
93
111
  function publish(args, server_opts) {
@@ -100,7 +118,7 @@ function publish(args, server_opts) {
100
118
  if (args.release) return sdk.removePrereleaseTag(template_dir);
101
119
  })
102
120
  .then(() => sdk.readAndValidateConfig(template_dir))
103
- .then((config) => {
121
+ .then(({config, warnings}) => {
104
122
  if (!config.id) log.die("The template’s template.yml doesn’t have an id. Add one and try again.");
105
123
 
106
124
  if (config.id.indexOf("/") > -1) {
@@ -143,6 +161,7 @@ function publish(args, server_opts) {
143
161
  log.victory(`Uploaded version ${external_version} on ${dt.toDateString()} at ${dt.toTimeString()}`,
144
162
  `Your template is available at ${protocol}://${server_opts.host}/@${template_path}/${external_version}`);
145
163
  }
164
+ warnings.forEach(warning => log.warn(warning));
146
165
  });
147
166
  })
148
167
  .catch((error) => {
package/lib/cmd/run.js CHANGED
@@ -17,7 +17,8 @@ function run(args) {
17
17
  port: port,
18
18
  listen: args.listen,
19
19
  open: args.open,
20
- debug: args.debug
20
+ debug: args.debug,
21
+ host: args.host
21
22
  });
22
23
  }
23
24
 
@@ -4,7 +4,7 @@ var log = require("../log"),
4
4
  sdk = require("../sdk");
5
5
 
6
6
  function version(args) {
7
- sdk.getSDKVersion()
7
+ sdk.getSdkVersion()
8
8
  .then((version_number) => console.log(version_number))
9
9
  .catch((error) => {
10
10
  if (args.debug) log.die("Failed to get SDK version number", error.message, error.stack);
package/lib/log.js CHANGED
@@ -12,7 +12,7 @@ function info(...lines) {
12
12
  for (let line of lines) console.log(colors.yellow(" " + line));
13
13
  }
14
14
  function warn(...lines) {
15
- for (let line of lines) console.warn(colors.yellow(" " + line));
15
+ for (let line of lines) console.warn(colors.yellow("⚠️ " + line));
16
16
  }
17
17
  function warn_bold(...lines) {
18
18
  for (let line of lines) console.warn(colors.bold.yellow(" " + line));
package/lib/sdk.js CHANGED
@@ -4,7 +4,8 @@ const fs = require("fs"),
4
4
  path = require("path"),
5
5
 
6
6
  cross_spawn = require("cross-spawn"),
7
- mod_request = require("request"),
7
+ fetch = require("node-fetch"),
8
+ FormData = require("form-data"),
8
9
  shell_quote = require("shell-quote"),
9
10
  yaml = require("js-yaml"),
10
11
  nodeResolve = require("resolve"),
@@ -21,7 +22,7 @@ const YAML_DUMP_OPTS = { flowLevel: 4 };
21
22
 
22
23
  const package_json_filename = path.join(__dirname, "..", "package.json");
23
24
  var sdk_version = null;
24
- function getSDKVersion() {
25
+ function getSdkVersion() {
25
26
  if (sdk_version) return Promise.resolve(sdk_version);
26
27
  return new Promise(function(resolve, reject) {
27
28
  fs.readFile(package_json_filename, "utf8", function(error, package_json) {
@@ -33,7 +34,7 @@ function getSDKVersion() {
33
34
  }
34
35
 
35
36
  function getSDKMajorVersion() {
36
- return getSDKVersion()
37
+ return getSdkVersion()
37
38
  .then((sdk_version) => {
38
39
  const version_tuple = sdk_version.split(".").map((x) => parseInt(x));
39
40
  return version_tuple[0];
@@ -94,77 +95,83 @@ const AUTHENTICATED_REQUEST_METHODS = new Set([
94
95
  "user/whoami"
95
96
  ]);
96
97
 
97
- const MULTIPART_REQUEST_METHODS = new Set([
98
- "template/publish",
99
- ]);
98
+ async function request(server_opts, method, data) {
99
+ let sdk_token;
100
100
 
101
- function request(server_opts, method, data) {
102
- let read_sdk_token_if_necessary;
103
101
  if (AUTHENTICATED_REQUEST_METHODS.has(method)) {
104
- read_sdk_token_if_necessary = getSdkToken(server_opts)
105
- .catch((error) => {
106
- log.problem(`Failed to read ${sdk_tokens_file}`, error.message);
107
- })
108
- .then((sdk_token) => {
109
- if (!sdk_token) {
110
- log.die("You are not logged in. Try ‘flourish login’ or ‘flourish register’ first.");
111
- }
112
- return sdk_token;
113
- });
114
- }
115
- else {
116
- read_sdk_token_if_necessary = Promise.resolve();
102
+ try {
103
+ sdk_token = await getSdkToken(server_opts);
104
+ }
105
+ catch (error) {
106
+ log.problem(`Failed to read ${sdk_tokens_file}`, error.message);
107
+ }
108
+ if (!sdk_token) {
109
+ log.die("You are not logged in. Try ‘flourish login’ or ‘flourish register’ first.");
110
+ }
117
111
  }
118
112
 
119
- return Promise.all([read_sdk_token_if_necessary, getSDKVersion()])
120
- .then(([sdk_token, sdk_version]) => new Promise(function(resolve, reject) {
121
- let protocol = "https";
122
- if (server_opts.host.match(/^(localhost|127\.0\.0\.1|.*\.local)(:\d+)?$/)) {
123
- protocol = "http";
124
- }
125
- let url = protocol + "://" + server_opts.host + "/api/v1/" + method;
126
- let request_params = {
127
- method: "POST",
128
- uri: url,
129
- };
130
-
131
- Object.assign(data, { sdk_token, sdk_version });
132
- if (server_opts.user) {
133
- request_params.auth = {
134
- user: server_opts.user,
135
- pass: server_opts.password,
136
- sendImmediately: true,
137
- };
138
- }
113
+ const sdk_version = await getSdkVersion();
114
+ const protocol = server_opts.host.match(/^(localhost|127\.0\.0\.1|.*\.local)(:\d+)?$/) ? "http:" : "https:";
115
+ const url = `${protocol}//${server_opts.host}/api/v1/${method}`;
116
+ const options = { method: data ? "POST" : "GET" };
139
117
 
140
- if (MULTIPART_REQUEST_METHODS.has(method)) {
141
- request_params.formData = data;
142
- }
143
- else {
144
- request_params.headers = { "Content-Type": "application/json" };
145
- request_params.body = JSON.stringify(data);
118
+ if (data) {
119
+ if (data instanceof FormData) {
120
+ if (sdk_token) {
121
+ data.append("sdk_token", sdk_token);
146
122
  }
123
+ data.append("sdk_version", sdk_version);
124
+ options.headers = data.getHeaders();
125
+ options.body = data;
126
+ }
127
+ else {
128
+ options.body = JSON.stringify({ ...data, sdk_token, sdk_version });
129
+ options.headers = { "content-type": "application/json" };
130
+ }
131
+ }
147
132
 
148
- mod_request(request_params, function(error, res) {
149
- if (error) log.die(error);
150
- if (res.statusCode == 200) {
151
- let r;
152
- try { r = JSON.parse(res.body); }
153
- catch (error) {
154
- log.die("Failed to parse response from server", error, res.body);
155
- }
156
- return resolve(r);
157
- }
133
+ if (server_opts.user) {
134
+ options.headers.authorization = Buffer.from(`${server_opts.user}:${server_opts.password}`).toString("base64");
135
+ }
158
136
 
159
- // We got an error response. See if we can parse it to extract an error message
160
- try {
161
- let r = JSON.parse(res.body);
162
- if ("error" in r) log.die("Error from server", r.error);
163
- }
164
- catch (e) { }
165
- log.die("Server error", res.body);
166
- });
167
- }));
137
+ let res;
138
+
139
+ try {
140
+ res = await fetch(url, options);
141
+ }
142
+ catch (e) {
143
+ log.die(e);
144
+ }
145
+
146
+ let text;
147
+
148
+ try {
149
+ // We could use res.json() here, but we're interested in what the body
150
+ // is when it's *not* json (load balancer issues etc.).
151
+ text = await res.text();
152
+ }
153
+ catch (error) {
154
+ log.die("Failed to get response from server", error);
155
+ }
156
+
157
+ let body;
158
+
159
+ try {
160
+ body = JSON.parse(text);
161
+ }
162
+ catch (error) {
163
+ log.die("Failed to parse response body", res.status, error, text);
164
+ }
165
+
166
+ if (res.ok) {
167
+ return body;
168
+ }
169
+
170
+ if (body.error) {
171
+ log.die("Error from server", res.status, body.error);
172
+ }
173
+
174
+ log.die("Server error", res.status, body);
168
175
  }
169
176
 
170
177
  function runBuildCommand(template_dir, command, node_env) {
@@ -383,6 +390,26 @@ async function resolveImports(config, template_dir) {
383
390
  return config;
384
391
  }
385
392
 
393
+ // Sets a default binding data_type of string in templates with both typed and untyped bindings, and return a post-publish warning message.
394
+ function checkDataTypes(config) {
395
+ const warnings = [];
396
+ if (!config.data) return { config, warnings };
397
+
398
+ const all_bindings = config.data.filter(binding => typeof binding !== "string"); // filter out title and description strings
399
+ const any_bindings_are_typed = all_bindings.some(binding => binding.data_type);
400
+
401
+ if (any_bindings_are_typed) {
402
+ config.data.forEach(binding => {
403
+ if (typeof binding === "string") return;
404
+ if (!binding.data_type) {
405
+ binding.data_type = "string";
406
+ warnings.push(`Missing data_type for key ${binding.key} in dataset ${binding.dataset} - assuming "string"`);
407
+ }
408
+ });
409
+ }
410
+ return { config, warnings };
411
+ }
412
+
386
413
  function readAndValidateConfig(template_dir) {
387
414
  return readConfig(template_dir)
388
415
  .then((config) => {
@@ -390,7 +417,8 @@ function readAndValidateConfig(template_dir) {
390
417
  return config;
391
418
  })
392
419
  .then(config => addShowConditions(config))
393
- .then(config => resolveImports(config, template_dir));
420
+ .then(config => resolveImports(config, template_dir))
421
+ .then(config => checkDataTypes(config));
394
422
  }
395
423
 
396
424
  function changeVersionNumberInPackageJson(template_dir, change_function) {
@@ -531,7 +559,7 @@ const TEMPLATE_SPECIAL = new Set([
531
559
  ]);
532
560
 
533
561
  module.exports = {
534
- checkTemplateVersion, getSDKVersion, getSDKMajorVersion,
562
+ checkTemplateVersion, getSdkVersion, getSDKMajorVersion,
535
563
 
536
564
  getSdkToken, setSdkToken, deleteSdkTokens,
537
565
  request,
@@ -227,7 +227,8 @@ function validateSetting(template_directory, conditional_settings, setting) {
227
227
  throw new Error(`template.yml setting “${property}” has unsupported width property: must be “full”, “half”, “quarter” or “three quarters”`);
228
228
  }
229
229
  if ("size" in setting) {
230
- if (setting.type !== "code" && setting.type !== "text") throw new Error(`template.yml setting “${property}” has a “size” setting but is not of type “string” or “code”`);
230
+ const can_set_size = setting.type == "code" || setting.type == "text" || (setting.type == "string" && setting.style == "buttons");
231
+ if (!can_set_size) throw new Error(`template.yml setting “${property}” has a “size” property; this requires type “text” or “code”, or type “string” with ”style: buttons”`);
231
232
  else if (setting.size !== "large") throw new Error(`template.yml setting “${property}” has unsupported size property: must be “large”`);
232
233
  }
233
234
  }
@@ -253,8 +254,8 @@ function validateSettings(template_directory, settings, bindings) {
253
254
  if (conditional_settings.size > 0) {
254
255
  conditional_settings.forEach(function(conditional_setting) {
255
256
  if (/^data\./.test(conditional_setting)) {
256
- if (!/^data\.\w+\.\w+$/.test(conditional_setting)) {
257
- throw new Error(`template.yml: “show_if” or “hide_if” property specifies invalid data binding “${conditional_setting}”`);
257
+ if (!/^data\.\w+\.\w+(\.type)?$/.test(conditional_setting)) {
258
+ throw new Error(`template.yml: “show_if” or “hide_if” property specifies invalid data binding or column type “${conditional_setting}”`);
258
259
  }
259
260
  if (!bindings || !Array.isArray(bindings)) {
260
261
  throw new Error(`template.yml: “show_if” or “hide_if” property refers to data binding “${conditional_setting}” when none are defined`);
@@ -271,7 +272,7 @@ function validateSettings(template_directory, settings, bindings) {
271
272
  }
272
273
  }
273
274
 
274
- function validateColSpec(spec, parser, data_table_names) {
275
+ function validateColSpec(spec, parser, data_table_names, is_optional) {
275
276
  const double_colon_ix = spec.indexOf("::");
276
277
  if (double_colon_ix == -1) throw new Error("Invalid data binding: " + spec);
277
278
  const data_table_name = spec.substr(0, double_colon_ix);
@@ -280,7 +281,7 @@ function validateColSpec(spec, parser, data_table_names) {
280
281
  }
281
282
 
282
283
  const col_spec = spec.substr(double_colon_ix + 2);
283
- parser(col_spec);
284
+ parser(col_spec, is_optional);
284
285
  }
285
286
 
286
287
  const VALID_DATA_BINDING_TYPES = new Set(["column", "columns"]);
@@ -320,7 +321,7 @@ function validateDataBinding(binding, data_table_names) {
320
321
  if (typeof binding.column !== "string") {
321
322
  throw new Error(`template.yml: “column” property of data binding “${binding_name}” must be a string`);
322
323
  }
323
- validateColSpec(binding.column, columns.parseColumn, data_table_names);
324
+ validateColSpec(binding.column, columns.parseColumn, data_table_names, binding.optional);
324
325
  }
325
326
  }
326
327
  else if (binding.type == "columns") {
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@flourish/sdk",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "The Flourish SDK",
5
5
  "module": "src/index.js",
6
6
  "scripts": {
7
7
  "prepare": "cd .. && make sdk_clean sdk",
8
- "test": "mocha --recursive"
8
+ "test": "mocha --recursive",
9
+ "audit": "check-audit",
10
+ "audit:resolve": "resolve-audit"
9
11
  },
10
12
  "bin": {
11
13
  "flourish": "bin/flourish"
@@ -14,32 +16,37 @@
14
16
  "license": "SEE LICENSE IN LICENSE.md",
15
17
  "repository": "kiln/flourish-sdk",
16
18
  "dependencies": {
19
+ "@flourish/interpreter": "^6.0.3",
17
20
  "@flourish/semver": "^1.0.1",
21
+ "@flourish/transform-data": "^2.1.0",
18
22
  "@handlebars/allow-prototype-access": "^1.0.3",
23
+ "@rollup/plugin-commonjs": "^17.1.0",
19
24
  "archiver": "^5.0.2",
20
25
  "chokidar": "^3.4.3",
21
26
  "colors": "^1.4.0",
22
27
  "cross-spawn": "^7.0.3",
23
28
  "d3-dsv": "^2.0.0",
24
29
  "express": "^4.17.1",
30
+ "form-data": "^4.0.0",
25
31
  "handlebars": "^4.7.6",
26
32
  "js-yaml": "^3.14.0",
27
33
  "minimist": "^1.2.5",
28
34
  "ncp": "^2.0.0",
35
+ "node-fetch": "^2.6.6",
29
36
  "parse5": "^6.0.1",
30
37
  "read": "^1.0.7",
31
- "request": "^2.88.2",
32
38
  "resolve": "^1.18.1",
33
39
  "rewrite-links": "^1.1.0",
34
40
  "rollup-plugin-terser": "^7.0.2",
35
41
  "shell-quote": "^1.7.2",
36
42
  "tmp": "^0.2.1",
37
- "ws": "^7.3.1"
43
+ "ws": "^7.4.6"
38
44
  },
39
45
  "devDependencies": {
40
46
  "@rollup/plugin-node-resolve": "^9.0.0",
41
47
  "d3-request": "^1.0.6",
42
- "mocha": "^8.2.0",
48
+ "mocha": "^9.1.2",
49
+ "npm-audit-resolver": "^2.3.0",
43
50
  "rollup": "^2.32.1",
44
51
  "sinon": "^9.2.0",
45
52
  "tempy": "^1.0.0"
package/server/columns.js CHANGED
@@ -6,17 +6,41 @@
6
6
 
7
7
  Object.defineProperty(exports, '__esModule', { value: true });
8
8
 
9
- function parseColumn(col_spec) {
9
+ const MAX_INTEGER = Math.pow(2, 31) - 1;
10
+ const MAX_RANGE_LENGTH = Math.pow(2, 15);
11
+
12
+ // Attempt to parse col_spec as a columns spec;
13
+ // return true if we succeed, and false if not.
14
+ function looksLikeMultipleColumns(col_spec) {
15
+ try {
16
+ parseColumns(col_spec);
17
+ }
18
+ catch (e) {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+
24
+ function parseColumn(col_spec, is_optional) {
10
25
  col_spec = col_spec.toUpperCase();
11
26
  if (!/^[A-Z]+$/.test(col_spec)) {
12
- throw new Error("Invalid column spec: " + col_spec);
27
+ if (!col_spec) {
28
+ if (is_optional) return -1; // Use -1 for unassigned optional binding
29
+ else throw new Error("Non-optional data binding must specify column");
30
+ }
31
+ if (looksLikeMultipleColumns(col_spec)) {
32
+ throw new Error("You can only select one column");
33
+ }
34
+ else throw new Error("Invalid column specification: " + col_spec);
13
35
  }
14
36
 
15
37
  var col_ix = 0;
16
38
  for (var i = 0; i < col_spec.length; i++) {
17
39
  col_ix = col_ix * 26 + (col_spec.charCodeAt(i) - 64);
18
40
  }
19
- return col_ix - 1;
41
+
42
+ if (col_ix - 1 > MAX_INTEGER) console.warn("Column index out of range");
43
+ return Math.min(col_ix - 1, MAX_INTEGER);
20
44
  }
21
45
 
22
46
  function printColumn(col_ix) {
@@ -51,6 +75,11 @@ function parseRange(col_range) {
51
75
  var incrementer = last_ix >= first_ix ? 1 : -1;
52
76
  var n = Math.abs(last_ix - first_ix) + 1;
53
77
 
78
+ if (n > MAX_RANGE_LENGTH) {
79
+ console.warn("Truncating excessively long range");
80
+ n = MAX_RANGE_LENGTH;
81
+ }
82
+
54
83
  for (var i = 0; i < n; i++) {
55
84
  r.push(first_ix + i*incrementer);
56
85
  }
@@ -83,18 +112,36 @@ function parseColumns(cols_spec) {
83
112
  }
84
113
 
85
114
  function splitIntoRanges(indexes) {
115
+ if (!indexes.length) {
116
+ return [];
117
+ }
86
118
  var ranges = [];
87
- var start, end;
88
- for (var i = 0; i < indexes.length; i++) {
89
- if (i > 0 && indexes[i] == indexes[i-1] + 1) {
90
- end = indexes[i];
119
+ var start = indexes[0], end = indexes[0];
120
+ var direction = null;
121
+ for (var i = 0; i < indexes.length - 1; i++) {
122
+ var diff = indexes[i + 1] - indexes[i];
123
+ if (direction === null && Math.abs(diff) === 1) {
124
+ // It's a range with either ascending columns (direction=1), or descending
125
+ // columns (direction=-1)
126
+ end = indexes[i + 1];
127
+ direction = diff;
128
+ continue;
91
129
  }
92
- else {
93
- if (typeof start != "undefined") ranges.push([start, end]);
94
- start = end = indexes[i];
130
+
131
+ if (diff === direction) {
132
+ // The range is continuing in the same direction as before
133
+ end = indexes[i + 1];
134
+ continue;
95
135
  }
136
+
137
+ // There's nothing more in the range, so add it, and start a new range from the
138
+ // next column
139
+ ranges.push([start, end]);
140
+ start = end = indexes[i + 1];
141
+ direction = null;
96
142
  }
97
- if (typeof start != "undefined") ranges.push([start, end]);
143
+ // There will always be a range which hasn't been added at the end
144
+ ranges.push([start, end]);
98
145
  return ranges;
99
146
  }
100
147
 
@@ -124,7 +171,7 @@ function parseDataBinding(d, data_table_ids) {
124
171
  r.data_table_id = data_table_ids[data_table_name];
125
172
 
126
173
  var col_spec = d[d.type].substr(double_colon_ix + 2);
127
- if (d.type == "column") r.column = parseColumn(col_spec);
174
+ if (d.type == "column") r.column = parseColumn(col_spec, d.optional);
128
175
  else if (d.type == "columns") r.columns = parseColumns(col_spec);
129
176
  else throw new Error("Unknown data binding type: " + d.type);
130
177
 
@@ -131,8 +131,6 @@ if (template && template.update && template.update.length != 0) {
131
131
  }
132
132
  `;
133
133
 
134
- module.exports = {
135
- withOriginCheck: BEFORE + CHECK_ORIGIN + AFTER,
136
- withoutOriginCheck: BEFORE + AFTER,
137
- validate: VALIDATE,
138
- };
134
+ exports.withOriginCheck = BEFORE + CHECK_ORIGIN + AFTER;
135
+ exports.withoutOriginCheck = BEFORE + AFTER;
136
+ exports.validate = VALIDATE;