@flourish/sdk 3.15.1 → 3.17.2
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 +12 -0
- package/lib/cmd/assign-version-number.js +1 -1
- package/lib/cmd/help.js +1 -1
- package/lib/cmd/publish.js +34 -13
- package/lib/cmd/run.js +2 -1
- package/lib/cmd/version.js +1 -1
- package/lib/log.js +1 -1
- package/lib/sdk.js +96 -68
- package/lib/validate_config.js +7 -6
- package/package.json +30 -22
- package/server/columns.js +59 -12
- package/server/comms_js.js +3 -5
- package/server/data.js +145 -8
- package/server/index.js +52 -20
- package/server/index_html.js +17 -23
- package/server/json.js +27 -1
- package/server/views/default_template_index.html +1 -1
- package/server/views/index.html +9 -4
- package/site/embedded.js +1 -1
- package/site/images/data_type_date.svg +6 -0
- package/site/images/data_type_number.svg +5 -0
- package/site/images/data_type_region.svg +4 -0
- package/site/images/data_type_string.svg +5 -0
- package/site/images/montage.jpg +0 -0
- package/site/script.js +2 -1
- package/site/sdk.css +1 -1
- package/site/talk_to_server.js +1 -1
- package/test/lib/sdk.js +9 -0
- package/test/lib/validate_config.js +7 -5
- package/utils/state.js +2 -0
- package/test/mocha.opts +0 -2
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# 3.17.2
|
|
2
|
+
* Fixes an issue which could cause template publication to fail
|
|
3
|
+
|
|
4
|
+
# 3.17.1
|
|
5
|
+
* Update version of @flourish/interpreter
|
|
6
|
+
|
|
7
|
+
# 3.17.0
|
|
8
|
+
* Allow published visualisations to be cloned into the SDK
|
|
9
|
+
|
|
10
|
+
# 3.16.0
|
|
11
|
+
* Upgrade all NPM dependencies.
|
|
12
|
+
|
|
1
13
|
# 3.15.1
|
|
2
14
|
* Avoid treating numeric-looking arguments as numbers.
|
|
3
15
|
|
|
@@ -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]
|
package/lib/cmd/publish.js
CHANGED
|
@@ -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" });
|
|
@@ -76,18 +95,19 @@ function zipUpTemplate(template_dir, config) {
|
|
|
76
95
|
});
|
|
77
96
|
}
|
|
78
97
|
|
|
79
|
-
function uploadTemplate(server_opts, template_id, external_version, zip_filename) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
98
|
+
async function uploadTemplate(server_opts, template_id, external_version, zip_filename) {
|
|
99
|
+
const body = new FormData();
|
|
100
|
+
const stat = await fs.promises.stat(zip_filename);
|
|
101
|
+
|
|
102
|
+
body.append("id", template_id);
|
|
103
|
+
body.append("version", external_version);
|
|
104
|
+
body.append("template", fs.createReadStream(zip_filename), {
|
|
105
|
+
contentType: "application/zip",
|
|
106
|
+
filename: "template.zip",
|
|
107
|
+
knownLength: stat.size
|
|
90
108
|
});
|
|
109
|
+
|
|
110
|
+
return sdk.request(server_opts, "template/publish", body);
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
function publish(args, server_opts) {
|
|
@@ -100,7 +120,7 @@ function publish(args, server_opts) {
|
|
|
100
120
|
if (args.release) return sdk.removePrereleaseTag(template_dir);
|
|
101
121
|
})
|
|
102
122
|
.then(() => sdk.readAndValidateConfig(template_dir))
|
|
103
|
-
.then((config) => {
|
|
123
|
+
.then(({config, warnings}) => {
|
|
104
124
|
if (!config.id) log.die("The template’s template.yml doesn’t have an id. Add one and try again.");
|
|
105
125
|
|
|
106
126
|
if (config.id.indexOf("/") > -1) {
|
|
@@ -143,6 +163,7 @@ function publish(args, server_opts) {
|
|
|
143
163
|
log.victory(`Uploaded version ${external_version} on ${dt.toDateString()} at ${dt.toTimeString()}`,
|
|
144
164
|
`Your template is available at ${protocol}://${server_opts.host}/@${template_path}/${external_version}`);
|
|
145
165
|
}
|
|
166
|
+
warnings.forEach(warning => log.warn(warning));
|
|
146
167
|
});
|
|
147
168
|
})
|
|
148
169
|
.catch((error) => {
|
package/lib/cmd/run.js
CHANGED
package/lib/cmd/version.js
CHANGED
|
@@ -4,7 +4,7 @@ var log = require("../log"),
|
|
|
4
4
|
sdk = require("../sdk");
|
|
5
5
|
|
|
6
6
|
function version(args) {
|
|
7
|
-
sdk.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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,
|
|
562
|
+
checkTemplateVersion, getSdkVersion, getSDKMajorVersion,
|
|
535
563
|
|
|
536
564
|
getSdkToken, setSdkToken, deleteSdkTokens,
|
|
537
565
|
request,
|
package/lib/validate_config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "3.17.2",
|
|
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"
|
|
8
|
+
"test": "mocha --recursive",
|
|
9
|
+
"audit": "check-audit",
|
|
10
|
+
"audit:resolve": "resolve-audit"
|
|
9
11
|
},
|
|
10
12
|
"bin": {
|
|
11
13
|
"flourish": "bin/flourish"
|
|
@@ -14,34 +16,40 @@
|
|
|
14
16
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
15
17
|
"repository": "kiln/flourish-sdk",
|
|
16
18
|
"dependencies": {
|
|
19
|
+
"@flourish/interpreter": "^6.0.4",
|
|
17
20
|
"@flourish/semver": "^1.0.1",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"@flourish/transform-data": "^2.1.0",
|
|
22
|
+
"@handlebars/allow-prototype-access": "^1.0.3",
|
|
23
|
+
"@rollup/plugin-commonjs": "^17.1.0",
|
|
24
|
+
"archiver": "^5.0.2",
|
|
25
|
+
"chokidar": "^3.4.3",
|
|
26
|
+
"colors": "^1.4.0",
|
|
27
|
+
"cross-spawn": "^7.0.3",
|
|
28
|
+
"d3-dsv": "^2.0.0",
|
|
23
29
|
"express": "^4.17.1",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
30
|
+
"form-data": "^4.0.0",
|
|
31
|
+
"handlebars": "^4.7.6",
|
|
32
|
+
"js-yaml": "^3.14.0",
|
|
33
|
+
"minimist": "^1.2.5",
|
|
27
34
|
"ncp": "^2.0.0",
|
|
28
|
-
"
|
|
35
|
+
"node-fetch": "^2.6.6",
|
|
36
|
+
"parse5": "^6.0.1",
|
|
29
37
|
"read": "^1.0.7",
|
|
30
|
-
"
|
|
31
|
-
"resolve": "^1.11.1",
|
|
38
|
+
"resolve": "^1.18.1",
|
|
32
39
|
"rewrite-links": "^1.1.0",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
40
|
+
"rollup-plugin-terser": "^7.0.2",
|
|
41
|
+
"shell-quote": "^1.7.2",
|
|
42
|
+
"tmp": "^0.2.1",
|
|
43
|
+
"ws": "^7.4.6"
|
|
36
44
|
},
|
|
37
45
|
"devDependencies": {
|
|
46
|
+
"@rollup/plugin-node-resolve": "^9.0.0",
|
|
38
47
|
"d3-request": "^1.0.6",
|
|
39
|
-
"mocha": "^
|
|
40
|
-
"
|
|
41
|
-
"rollup
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"tempy": "^0.3.0"
|
|
48
|
+
"mocha": "^9.1.2",
|
|
49
|
+
"npm-audit-resolver": "^2.3.0",
|
|
50
|
+
"rollup": "^2.32.1",
|
|
51
|
+
"sinon": "^9.2.0",
|
|
52
|
+
"tempy": "^1.0.0"
|
|
45
53
|
},
|
|
46
54
|
"engines": {
|
|
47
55
|
"node": ">=8.3"
|
package/server/columns.js
CHANGED
|
@@ -6,17 +6,41 @@
|
|
|
6
6
|
|
|
7
7
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
package/server/comms_js.js
CHANGED
|
@@ -131,8 +131,6 @@ if (template && template.update && template.update.length != 0) {
|
|
|
131
131
|
}
|
|
132
132
|
`;
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
validate: VALIDATE,
|
|
138
|
-
};
|
|
134
|
+
exports.withOriginCheck = BEFORE + CHECK_ORIGIN + AFTER;
|
|
135
|
+
exports.withoutOriginCheck = BEFORE + AFTER;
|
|
136
|
+
exports.validate = VALIDATE;
|