pinstripe 0.14.0 → 0.15.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/cli.js +1 -1
- package/lib/command.js +12 -0
- package/lib/commands/generate_static_site.js +8 -8
- package/lib/commands/generate_view.js +4 -3
- package/lib/commands/purge_old_sessions.js +12 -0
- package/lib/commands/start_server.js +10 -8
- package/lib/database/adapters/mysql.js +33 -2
- package/lib/database/adapters/sqlite.js +36 -5
- package/lib/database/row.js +3 -2
- package/lib/database.js +12 -1
- package/lib/extensions/_importer.js +2 -0
- package/lib/extensions/multi-tenant/database/table.js +18 -0
- package/lib/extensions/multi-tenant/database.js +6 -0
- package/lib/extensions/multi-tenant/index.js +4 -0
- package/lib/extensions/multi-tenant/migrations/1627976174_create_tenant_table_and_add_tenant_id_to_existing_tables.js +16 -0
- package/lib/extensions/multi-tenant/migrations/_importer.js +2 -0
- package/lib/extensions/multi-tenant/models/_importer.js +2 -0
- package/lib/extensions/multi-tenant/models/tenant.js +7 -0
- package/lib/extensions/multi-tenant/services/_importer.js +2 -0
- package/lib/extensions/multi-tenant/services/tenant.js +7 -0
- package/lib/import_all.js +1 -0
- package/lib/migrations/1628057822_create_session.js +1 -0
- package/lib/services/app.js +2 -0
- package/lib/services/bot.js +69 -0
- package/lib/services/fetch.js +4 -5
- package/lib/services/params.js +6 -1
- package/lib/services/session.js +8 -2
- package/lib/services/stylesheets.js +16 -0
- package/lib/url.js +2 -2
- package/lib/views/{_layout.js → main/_layout.js} +2 -2
- package/lib/views/{blocks → main/blocks}/default.js +5 -7
- package/lib/views/{blocks → main/blocks}/help.js +0 -0
- package/lib/views/{bundle.js.js → main/bundle.js.js} +1 -1
- package/lib/views/{sign_in.js → main/sign_in.js} +2 -1
- package/lib/views/{sign_out.js → main/sign_out.js} +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/button.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/card.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/form.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/frame.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/input.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/label.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/markdown_editor.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/modal.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/overlay.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/pagination.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/progress_bar.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/components/textarea.css.js +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/global.css +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/reset.css +0 -0
- package/lib/views/{stylesheets → main/stylesheets}/vars.css +0 -0
- package/package.json +6 -5
- package/lib/services/stylesheet_view_names.js +0 -10
package/cli.js
CHANGED
package/lib/command.js
CHANGED
|
@@ -19,6 +19,18 @@ export const Command = Base.extend().include({
|
|
|
19
19
|
|
|
20
20
|
run(name = 'list-commands', ...args){
|
|
21
21
|
return this.create(name, ...args).run();
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
get schedules(){
|
|
25
|
+
if(!this.hasOwnProperty('_schedules')){
|
|
26
|
+
this._schedules = [];
|
|
27
|
+
}
|
|
28
|
+
return this._schedules;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
schedule(...args){
|
|
32
|
+
this.schedules.push(args);
|
|
33
|
+
return this;
|
|
22
34
|
}
|
|
23
35
|
});
|
|
24
36
|
},
|
|
@@ -8,12 +8,12 @@ export default {
|
|
|
8
8
|
|
|
9
9
|
async run(){
|
|
10
10
|
this.pages = {};
|
|
11
|
-
const
|
|
12
|
-
return `/${path.replace(/(^|\/)index$/, '')}`.replace(/^\/+/, '/');
|
|
11
|
+
const urls = Object.keys(View.classes).filter(path => !path.match(/(^|\/)_/)).map(path => {
|
|
12
|
+
return Url.fromString(`/${path.replace(/(^|\/)index$/, '')}`.replace(/^\/+/, '/'));
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
while(
|
|
16
|
-
await this.crawlPage({
|
|
15
|
+
while(urls.length){
|
|
16
|
+
await this.crawlPage({ _url: urls.shift() });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const pages = Object.values(this.pages).filter(page => page.status == 200 && Object.keys(page.params).length == 1);
|
|
@@ -25,7 +25,7 @@ export default {
|
|
|
25
25
|
const { params, headers } = pages.shift();
|
|
26
26
|
const contentType = headers['content-type'];
|
|
27
27
|
|
|
28
|
-
const path = params.
|
|
28
|
+
const path = params._url.path;
|
|
29
29
|
let filePath = path.replace(/^\//, '');
|
|
30
30
|
if(filePath.match(/(^|\/)$/)){
|
|
31
31
|
filePath = `${filePath}index`;
|
|
@@ -34,7 +34,7 @@ export default {
|
|
|
34
34
|
filePath = `${filePath}.${mimeTypes.extension(contentType)}`
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const data = (await this.fetch({
|
|
37
|
+
const data = (await this.fetch({ _url: Url.fromString(path) }))[2];
|
|
38
38
|
|
|
39
39
|
await generateFile(filePath, () => {
|
|
40
40
|
echo(data.join(''));
|
|
@@ -58,7 +58,7 @@ export default {
|
|
|
58
58
|
const urls = this.extractUrls(virtualDom);
|
|
59
59
|
while(urls.length){
|
|
60
60
|
const url = urls.shift();
|
|
61
|
-
await this.crawlPage({ ...url.params,
|
|
61
|
+
await this.crawlPage({ ...url.params, _url: url });
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@ export default {
|
|
|
68
68
|
['src', 'href'].forEach(name => {
|
|
69
69
|
const value = attributes[name];
|
|
70
70
|
if(!value) return;
|
|
71
|
-
const url = Url.fromString(value
|
|
71
|
+
const url = Url.fromString(value);
|
|
72
72
|
if(url.host != 'localhost') return;
|
|
73
73
|
out.push(url);
|
|
74
74
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
export default async ({
|
|
3
3
|
cliUtils: { extractArg },
|
|
4
|
-
fsBuilder: { inProjectRootDir, generateFile, line, indent }
|
|
4
|
+
fsBuilder: { inProjectRootDir, generateFile, line, indent },
|
|
5
|
+
app
|
|
5
6
|
}) => {
|
|
6
7
|
let name = extractArg('').replace(/^\//, '');
|
|
7
8
|
if(name == ''){
|
|
@@ -10,7 +11,7 @@ export default async ({
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
if(!name.match(/\.[^\/]+$/)){
|
|
13
|
-
name = `${name}.
|
|
14
|
+
name = `${name}.js`;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
await inProjectRootDir(async () => {
|
|
@@ -21,7 +22,7 @@ export default async ({
|
|
|
21
22
|
line();
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
await generateFile(`lib/views/${name}`, () => {
|
|
25
|
+
await generateFile(`lib/views/${await app}/${name}`, () => {
|
|
25
26
|
line();
|
|
26
27
|
line('export default ({ renderHtml, params }) => renderHtml(`');
|
|
27
28
|
indent(() => {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import Busboy from 'busboy';
|
|
4
|
-
import {
|
|
5
|
-
const { parse: parseQueryString } = qs;
|
|
4
|
+
import { Url } from '../url.js';
|
|
6
5
|
|
|
7
|
-
export default async ({ fetch }) => {
|
|
6
|
+
export default async ({ fetch, bot }) => {
|
|
8
7
|
const host = process.env.HOST || '127.0.0.1';
|
|
9
8
|
const port = process.env.PORT || 3000;
|
|
10
9
|
|
|
@@ -27,20 +26,23 @@ export default async ({ fetch }) => {
|
|
|
27
26
|
}).listen(port, host, () => {
|
|
28
27
|
console.log(`Pinstripe running at http://${host}:${port}/`)
|
|
29
28
|
});
|
|
29
|
+
|
|
30
|
+
bot.start();
|
|
31
|
+
|
|
32
|
+
return new Promise(() => {});
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
const extractParams = async (request) => {
|
|
33
36
|
const { method, url, headers } = request;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const queryString = parseQueryString(matches ? matches[2] : "");
|
|
37
|
+
const _url = Url.fromString(url);
|
|
38
|
+
if(headers['x-host']) _url.host = headers['x-host'];
|
|
37
39
|
const body = method.match(/^POST|PUT|PATCH$/) ? await parseBody(request) : {};
|
|
38
40
|
|
|
39
41
|
return {
|
|
40
|
-
...
|
|
42
|
+
..._url.params,
|
|
41
43
|
...body,
|
|
42
44
|
_method: method,
|
|
43
|
-
|
|
45
|
+
_url,
|
|
44
46
|
_headers: headers
|
|
45
47
|
};
|
|
46
48
|
};
|
|
@@ -207,7 +207,7 @@ Adapter.register('mysql').include({
|
|
|
207
207
|
const table = this._table;
|
|
208
208
|
const database = table._database;
|
|
209
209
|
|
|
210
|
-
await table.create();
|
|
210
|
+
if(!await table.exists()) await table.create();
|
|
211
211
|
|
|
212
212
|
if(!await this.exists()){
|
|
213
213
|
await database.run`
|
|
@@ -281,9 +281,20 @@ Adapter.register('mysql').include({
|
|
|
281
281
|
return this;
|
|
282
282
|
},
|
|
283
283
|
|
|
284
|
-
_generateInsertSql(){
|
|
284
|
+
async _generateInsertSql(){
|
|
285
285
|
this._fields['id'] = crypto.randomUUID();
|
|
286
286
|
this._alteredFields['id'] = this._fields['id'];
|
|
287
|
+
|
|
288
|
+
const database = await this._database;
|
|
289
|
+
const { isMultiTenant } = database;
|
|
290
|
+
const isScopedToTenant = isMultiTenant && !this.constructor.name.match(/^(pinstripe[A-Z]|tenant$)/);
|
|
291
|
+
const tenant = isScopedToTenant && database._environment ? await database._environment.tenant : undefined;
|
|
292
|
+
if(tenant){
|
|
293
|
+
this._fields['tenantId'] = tenant.id;
|
|
294
|
+
this._alteredFields['tenantId'] = tenant.id;
|
|
295
|
+
} else if(isScopedToTenant) {
|
|
296
|
+
return this._database.renderSql`select NULL`;
|
|
297
|
+
}
|
|
287
298
|
|
|
288
299
|
return this._database.renderSql`
|
|
289
300
|
insert into ${this._adapter.escapeIdentifier(Inflector.pluralize(this.constructor.name))}(
|
|
@@ -486,12 +497,32 @@ Adapter.register('mysql').include({
|
|
|
486
497
|
out.push(this.renderSql` from ${joinRoot._fromSql}`);
|
|
487
498
|
}
|
|
488
499
|
|
|
500
|
+
const { isMultiTenant } = this._database;
|
|
501
|
+
const isScopedToTenant = isMultiTenant && !this.constructor.name.match(/^(pinstripe[A-Z]|tenants$)/);
|
|
502
|
+
const tenant = isScopedToTenant ? await this._database._environment.tenant : undefined;
|
|
503
|
+
|
|
489
504
|
if (options.hasOwnProperty('where')){
|
|
490
505
|
if(options.where){
|
|
491
506
|
out.push(this.renderSql` where ${options.where}`);
|
|
507
|
+
if(tenant){
|
|
508
|
+
out.push(this.renderSql` and ${this.tenantId} = uuid_to_bin(${tenant.id})`);
|
|
509
|
+
} else if(isScopedToTenant){
|
|
510
|
+
out.push(this.renderSql` and 1 = 2`);
|
|
511
|
+
}
|
|
512
|
+
} else if(isScopedToTenant){
|
|
513
|
+
out.push(this.renderSql` where 1 = 2`);
|
|
492
514
|
}
|
|
493
515
|
} else if(joinRoot._whereSql.length) {
|
|
494
516
|
out.push(this.renderSql` where ${joinRoot._whereSql}`);
|
|
517
|
+
if(tenant){
|
|
518
|
+
out.push(this.renderSql` and ${this.tenantId} = uuid_to_bin(${tenant.id})`);
|
|
519
|
+
} else if(isScopedToTenant){
|
|
520
|
+
out.push(this.renderSql` and 1 = 2`);
|
|
521
|
+
}
|
|
522
|
+
} else if(tenant){
|
|
523
|
+
out.push(this.renderSql` where ${this.tenantId} = uuid_to_bin(${tenant.id})`);
|
|
524
|
+
} else if(isScopedToTenant){
|
|
525
|
+
out.push(this.renderSql` where 1 = 2`);
|
|
495
526
|
}
|
|
496
527
|
|
|
497
528
|
if(options.hasOwnProperty('orderBy')){
|
|
@@ -182,7 +182,7 @@ Adapter.register('sqlite').include({
|
|
|
182
182
|
const table = this._table;
|
|
183
183
|
const database = table._database;
|
|
184
184
|
|
|
185
|
-
await table.create();
|
|
185
|
+
if(!await table.exists()) await table.create();
|
|
186
186
|
|
|
187
187
|
if(!await this.exists()){
|
|
188
188
|
let defaultSql = options.default;
|
|
@@ -265,10 +265,21 @@ Adapter.register('sqlite').include({
|
|
|
265
265
|
return this;
|
|
266
266
|
},
|
|
267
267
|
|
|
268
|
-
_generateInsertSql(){
|
|
268
|
+
async _generateInsertSql(){
|
|
269
269
|
this._fields['id'] = crypto.randomUUID();
|
|
270
270
|
this._alteredFields['id'] = this._fields['id'];
|
|
271
|
-
|
|
271
|
+
|
|
272
|
+
const database = await this._database;
|
|
273
|
+
const { isMultiTenant } = database;
|
|
274
|
+
const isScopedToTenant = isMultiTenant && !this.constructor.name.match(/^(pinstripe[A-Z]|tenant$)/);
|
|
275
|
+
const tenant = isScopedToTenant && database._environment ? await database._environment.tenant : undefined;
|
|
276
|
+
if(tenant){
|
|
277
|
+
this._fields['tenantId'] = tenant.id;
|
|
278
|
+
this._alteredFields['tenantId'] = tenant.id;
|
|
279
|
+
} else if(isScopedToTenant) {
|
|
280
|
+
return this._database.renderSql`select NULL`;
|
|
281
|
+
}
|
|
282
|
+
|
|
272
283
|
return this._database.renderSql`
|
|
273
284
|
insert into ${this._adapter.escapeIdentifier(Inflector.pluralize(this.constructor.name))}(
|
|
274
285
|
${Object.keys(this._alteredFields).map((key, i) =>
|
|
@@ -459,23 +470,43 @@ Adapter.register('sqlite').include({
|
|
|
459
470
|
}
|
|
460
471
|
out.push(this.renderSql`${Inflector.singularize(this.constructor.name)} as \`_type\``);
|
|
461
472
|
}
|
|
462
|
-
|
|
463
|
-
const joinRoot = this._joinRoot;
|
|
464
473
|
|
|
474
|
+
const joinRoot = this._joinRoot;
|
|
465
475
|
if(options.hasOwnProperty('from')){
|
|
466
476
|
if(options.from){
|
|
467
477
|
out.push(this.renderSql` from ${options.from}`);
|
|
468
478
|
}
|
|
469
479
|
} else {
|
|
470
480
|
out.push(this.renderSql` from ${joinRoot._fromSql}`);
|
|
481
|
+
|
|
471
482
|
}
|
|
483
|
+
|
|
484
|
+
const { isMultiTenant } = this._database;
|
|
485
|
+
const isScopedToTenant = isMultiTenant && !this.constructor.name.match(/^(pinstripe[A-Z]|tenants$)/);
|
|
486
|
+
const tenant = isScopedToTenant ? await this._database._environment.tenant : undefined;
|
|
472
487
|
|
|
473
488
|
if (options.hasOwnProperty('where')){
|
|
474
489
|
if(options.where){
|
|
475
490
|
out.push(this.renderSql` where ${options.where}`);
|
|
491
|
+
if(tenant){
|
|
492
|
+
out.push(this.renderSql` and ${this.tenantId} = ${tenant.id}`);
|
|
493
|
+
} else if(isScopedToTenant){
|
|
494
|
+
out.push(this.renderSql` where 1 = 2`);
|
|
495
|
+
}
|
|
496
|
+
} else if(isScopedToTenant){
|
|
497
|
+
out.push(this.renderSql` where 1 = 2`);
|
|
476
498
|
}
|
|
477
499
|
} else if(joinRoot._whereSql.length) {
|
|
478
500
|
out.push(this.renderSql` where ${joinRoot._whereSql}`);
|
|
501
|
+
if(tenant){
|
|
502
|
+
out.push(this.renderSql` and ${this.tenantId} = ${tenant.id}`);
|
|
503
|
+
} else if(isScopedToTenant){
|
|
504
|
+
out.push(this.renderSql` and 1 = 2`);
|
|
505
|
+
}
|
|
506
|
+
} else if(tenant){
|
|
507
|
+
out.push(this.renderSql` where ${this.tenantId} = ${tenant.id}`);
|
|
508
|
+
} else if(isScopedToTenant){
|
|
509
|
+
out.push(this.renderSql` where 1 = 2`);
|
|
479
510
|
}
|
|
480
511
|
|
|
481
512
|
if(options.hasOwnProperty('orderBy')){
|
package/lib/database/row.js
CHANGED
|
@@ -187,9 +187,10 @@ export const Row = Base.extend().include({
|
|
|
187
187
|
},
|
|
188
188
|
|
|
189
189
|
__setMissing(name, value){
|
|
190
|
-
if(name == 'id'){
|
|
191
|
-
throw
|
|
190
|
+
if(name == 'id' || name == 'tenantId'){
|
|
191
|
+
throw `'${name}' fields can't be set directly on a row`;
|
|
192
192
|
}
|
|
193
|
+
|
|
193
194
|
const normalizedValue = this._normalizeField(name, value)
|
|
194
195
|
if(this._fields[name] != normalizedValue){
|
|
195
196
|
this._alteredFields[name] = normalizedValue;
|
package/lib/database.js
CHANGED
|
@@ -9,7 +9,8 @@ import { Adapter, createAdapterDeligator } from './database/adapter.js';
|
|
|
9
9
|
const deligateToAdapter = createAdapterDeligator('database');
|
|
10
10
|
|
|
11
11
|
export const Database = Base.extend().include({
|
|
12
|
-
async initialize({ environment, config }){
|
|
12
|
+
async initialize({ environment, config }, skipInit = false){
|
|
13
|
+
if(skipInit) return
|
|
13
14
|
this._environment = environment;
|
|
14
15
|
const { adapter = 'mysql', ...adapterConfig } = await config.database;
|
|
15
16
|
this._adapter = Adapter.create(adapter, adapterConfig);
|
|
@@ -21,6 +22,16 @@ export const Database = Base.extend().include({
|
|
|
21
22
|
this._isInitialized = true;
|
|
22
23
|
},
|
|
23
24
|
|
|
25
|
+
isMultiTenant: false,
|
|
26
|
+
|
|
27
|
+
get withoutMultiTenancy(){
|
|
28
|
+
return (async () => {
|
|
29
|
+
const out = await Database.new({}, true);
|
|
30
|
+
out.assignProps(this, { isMultiTenant: false });
|
|
31
|
+
return out;
|
|
32
|
+
})();
|
|
33
|
+
},
|
|
34
|
+
|
|
24
35
|
renderSql: deligateToAdapter('renderSql'),
|
|
25
36
|
|
|
26
37
|
toSql: deligateToAdapter('toSql'),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { Table } from '../../../database/table.js';
|
|
3
|
+
|
|
4
|
+
Table.include({
|
|
5
|
+
meta(){
|
|
6
|
+
const { create } = this.prototype;
|
|
7
|
+
|
|
8
|
+
this.include({
|
|
9
|
+
async create(...args){
|
|
10
|
+
const out = await create.call(this, ...args);
|
|
11
|
+
if(this.constructor.name.match(/^pinstripe[A-Z]/)) return out;
|
|
12
|
+
if(this.constructor.name == 'tenants') return out;
|
|
13
|
+
await this.addColumn('tenantId', 'foreign_key');
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
export default async ({ tenants, database }) => {
|
|
3
|
+
|
|
4
|
+
await tenants.addColumn('name', 'string', { index: true });
|
|
5
|
+
await tenants.addColumn('host', 'string', { index: true });
|
|
6
|
+
|
|
7
|
+
const tableNames = Object.keys(await database.tables());
|
|
8
|
+
|
|
9
|
+
while(tableNames.length){
|
|
10
|
+
const tableName = tableNames.shift();
|
|
11
|
+
if(tableName.match(/^pinstripe[A-Z]/)) continue;
|
|
12
|
+
if(tableName == 'tenants') continue;
|
|
13
|
+
if(await database[tableName].tenantId.exists()) continue;
|
|
14
|
+
await database[tableName].addColumn('tenantId', 'foreign_key');
|
|
15
|
+
}
|
|
16
|
+
};
|
package/lib/import_all.js
CHANGED
|
@@ -62,6 +62,7 @@ const importAllRecursive = async (dirPath, importer = defaultImporter) => {
|
|
|
62
62
|
const importerFilePath = `${current}/_importer.js`;
|
|
63
63
|
if(existsSync(importerFilePath)){
|
|
64
64
|
const importerFactory = await ( await import(importerFilePath) ).default;
|
|
65
|
+
if(!importerFactory) continue;
|
|
65
66
|
const importer = await importerFactory(current);
|
|
66
67
|
await importAllRecursive(current, importer);
|
|
67
68
|
} else {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
import cronParser from 'cron-parser';
|
|
3
|
+
|
|
4
|
+
import { Command } from '../command.js';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
|
|
8
|
+
meta(){
|
|
9
|
+
this.scope = 'root';
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
start(){
|
|
13
|
+
if(this._loop) this._loop;
|
|
14
|
+
|
|
15
|
+
this._loop = new Promise(async resolve => {
|
|
16
|
+
let current = getUnixTime();
|
|
17
|
+
while(true){
|
|
18
|
+
const target = getUnixTime();
|
|
19
|
+
while(current < target){
|
|
20
|
+
current++;
|
|
21
|
+
await this.runCommands(current);
|
|
22
|
+
}
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
24
|
+
|
|
25
|
+
if(!this._loop) break;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resolve();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return this._loop;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async stop(){
|
|
35
|
+
const loop = this._loop;
|
|
36
|
+
delete this._loop;
|
|
37
|
+
await loop;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async runCommands(unixTime){
|
|
41
|
+
const currentDate = new Date(unixTime * 1000);
|
|
42
|
+
const endDate = new Date((unixTime + 1) * 1000);
|
|
43
|
+
const commands = Object.values(Command.classes);
|
|
44
|
+
while(commands.length){
|
|
45
|
+
const command = commands.shift();
|
|
46
|
+
const schedules = [ ...command.schedules ];
|
|
47
|
+
while(schedules.length){
|
|
48
|
+
const [ crontab, ...args ] = schedules.shift();
|
|
49
|
+
const interval = cronParser.parseExpression(crontab, {
|
|
50
|
+
currentDate,
|
|
51
|
+
endDate
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if(interval.hasNext()){
|
|
55
|
+
await this.runCommand(command.name, ...args);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
destroy(){
|
|
64
|
+
return this.stop();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getUnixTime = () => Math.floor(Date.now() / 1000);
|
package/lib/services/fetch.js
CHANGED
|
@@ -3,9 +3,8 @@ import { View } from '../view.js';
|
|
|
3
3
|
export default ({ createEnvironment }) => {
|
|
4
4
|
return (_params = {}) => createEnvironment(async ({ environment, renderView }) => {
|
|
5
5
|
const params = normalizeParams(_params);
|
|
6
|
-
const viewName = params._path.replace(/^\/|\/$/g, '');
|
|
7
|
-
|
|
8
6
|
environment.params = params;
|
|
7
|
+
const viewName = `${await environment.app}${params._url.path}`.replace(/^\/|\/$/g, '');
|
|
9
8
|
|
|
10
9
|
let out = await renderGuardViews(renderView, viewName, params);
|
|
11
10
|
if(out){
|
|
@@ -62,7 +61,7 @@ const renderDefaultViews = async (renderView, viewName, params) => {
|
|
|
62
61
|
while(true){
|
|
63
62
|
const out = normalizeResponse(await renderView(prefixSegments.length ? [...prefixSegments, 'default'].join('/') : 'default', {
|
|
64
63
|
...params,
|
|
65
|
-
_pathOffset: params.
|
|
64
|
+
_pathOffset: params._url.path.substr(`/${prefixSegments.join('/')}`.length).replace(/^\//, '')
|
|
66
65
|
}));
|
|
67
66
|
if(out){
|
|
68
67
|
return out;
|
|
@@ -79,8 +78,8 @@ const normalizeParams = (params) => {
|
|
|
79
78
|
if(!out._method){
|
|
80
79
|
out._method = 'get';
|
|
81
80
|
}
|
|
82
|
-
if(!params.
|
|
83
|
-
out.
|
|
81
|
+
if(!params._url){
|
|
82
|
+
out._url = Url.new();
|
|
84
83
|
}
|
|
85
84
|
if(!params._headers){
|
|
86
85
|
out._headers = {};
|
package/lib/services/params.js
CHANGED
package/lib/services/session.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
|
|
2
|
-
export default ({ cookies, database }) => {
|
|
2
|
+
export default async ({ cookies, database }) => {
|
|
3
3
|
const { pinstripeSession } = cookies;
|
|
4
4
|
if(!pinstripeSession){
|
|
5
5
|
return;
|
|
6
6
|
}
|
|
7
7
|
const [ sessionId, passString ] = pinstripeSession.split(/:/);
|
|
8
|
-
|
|
8
|
+
const session = await database.sessions.idEq(sessionId).passStringEq(passString).first();
|
|
9
|
+
if(session && session.lastAccessedAt < (Date.now() - 1000 * 60 * 5)){
|
|
10
|
+
await session.update({
|
|
11
|
+
lastAccessedAt: Date.now()
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return session;
|
|
9
15
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
export default async ({ viewNames, environment }) => {
|
|
3
|
+
const app = await environment.app;
|
|
4
|
+
const prefixStylesheets = [
|
|
5
|
+
'stylesheets/vars.css',
|
|
6
|
+
'stylesheets/reset.css',
|
|
7
|
+
'stylesheets/global.css',
|
|
8
|
+
];
|
|
9
|
+
const componentStylesheetViewNames = viewNames.filter(viewName => {
|
|
10
|
+
const isCss = viewName.match(/\.css$/);
|
|
11
|
+
const isNotPrefixStylesheet = !prefixStylesheets.includes(viewName.replace(/^[^\/]*\//, ''))
|
|
12
|
+
const isAppView = viewName.startsWith(`${app}/`);
|
|
13
|
+
return isCss && isNotPrefixStylesheet && isAppView;
|
|
14
|
+
}).map(viewName => viewName.replace(/^[^\/]*\//, ''))
|
|
15
|
+
return [ ...prefixStylesheets, ...componentStylesheetViewNames ];
|
|
16
|
+
};
|
package/lib/url.js
CHANGED
|
@@ -5,11 +5,11 @@ import { StringReader } from './string_reader.js';
|
|
|
5
5
|
export const Url = Base.extend().include({
|
|
6
6
|
meta(){
|
|
7
7
|
this.assignProps({
|
|
8
|
-
fromString(url, referenceUrl){
|
|
8
|
+
fromString(url, referenceUrl = 'http://localhost'){
|
|
9
9
|
const out = new Url()
|
|
10
10
|
url = new StringReader(url)
|
|
11
11
|
if(!(referenceUrl instanceof Url)){
|
|
12
|
-
referenceUrl = Url.fromString(referenceUrl
|
|
12
|
+
referenceUrl = Url.fromString(referenceUrl, new Url())
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
let matches;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
export default async ({ params: { title, body, isSignedIn, user }, renderHtml,
|
|
2
|
+
export default async ({ params: { title, body, isSignedIn, user }, renderHtml, stylesheets }) => renderHtml`
|
|
3
3
|
<!DOCTYPE html>
|
|
4
4
|
<html>
|
|
5
5
|
<head>
|
|
6
6
|
<title>${title}</title>
|
|
7
|
-
${
|
|
7
|
+
${stylesheets.map(viewName => renderHtml`
|
|
8
8
|
<link rel="stylesheet" href="/${viewName}">
|
|
9
9
|
`)}
|
|
10
10
|
<script src="/bundle.js"></script>
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
export default ({ renderHtml, viewNames, params }) => {
|
|
3
|
+
const path = params._url.path
|
|
4
|
+
const name = path.replace(/\/blocks\//, '');
|
|
5
|
+
const prefix = path.replace(/^\/blocks\//, '');
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
const name = _path.replace(/\/blocks\//, '');
|
|
6
|
-
const prefix = _path.replace(/^\/blocks\//, '');
|
|
7
|
-
|
|
8
|
-
const names = Object
|
|
9
|
-
.keys(View.classes)
|
|
7
|
+
const names = viewNames
|
|
10
8
|
.filter(name => name.match(/^blocks\/.+/) && name != 'blocks/default')
|
|
11
9
|
.map(name => name.replace(/^blocks\//, ''))
|
|
12
10
|
.filter(name => name.startsWith(prefix))
|
|
File without changes
|
|
@@ -25,7 +25,8 @@ export default async ({ renderForm, database, renderHtml }) => renderForm({
|
|
|
25
25
|
const passString = crypto.randomUUID();
|
|
26
26
|
const session = await database.sessions.insert({
|
|
27
27
|
userId: id,
|
|
28
|
-
passString
|
|
28
|
+
passString,
|
|
29
|
+
lastAccessedAt: Date.now()
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
const [ status, headers, body ] = await renderHtml`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "pinstripe",
|
|
4
4
|
"description": "Pinstripe is a fullstack JavaScript web framework for Node.js.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.15.0",
|
|
6
6
|
"author": "Jody Salt",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./lib/index.js"
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./multi-tenant": "./lib/extensions/multi-tenant/index.js"
|
|
10
11
|
},
|
|
11
12
|
"bin": {
|
|
12
13
|
"pinstripe": "./cli.js"
|
|
@@ -18,13 +19,14 @@
|
|
|
18
19
|
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest lib"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"jest": "^
|
|
22
|
+
"jest": "^28.0.1",
|
|
22
23
|
"ramda": "^0.27.0"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"bcrypt": "^5.0.1",
|
|
26
27
|
"busboy": "^0.3.1",
|
|
27
28
|
"chalk": "^4.1.0",
|
|
29
|
+
"cron-parser": "^4.3.0",
|
|
28
30
|
"html-entities": "^2.3.2",
|
|
29
31
|
"js-yaml": "^4.1.0",
|
|
30
32
|
"luxon": "^2.3.1",
|
|
@@ -33,10 +35,9 @@
|
|
|
33
35
|
"mime-types": "^2.1.28",
|
|
34
36
|
"mysql2": "^2.3.3",
|
|
35
37
|
"nodemailer": "^6.7.2",
|
|
36
|
-
"qs": "^6.9.4",
|
|
37
38
|
"sqlite3": "^5.0.2",
|
|
38
39
|
"unionfs": "^4.4.0",
|
|
39
40
|
"webpack": "^5.39.1"
|
|
40
41
|
},
|
|
41
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "f68a5a5143098d3d42d60c5ba64e89fbf2db7ebe"
|
|
42
43
|
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export default ({ viewNames }) => {
|
|
3
|
-
const nonComponentStylesheetViewNames = [
|
|
4
|
-
'stylesheets/vars.css',
|
|
5
|
-
'stylesheets/reset.css',
|
|
6
|
-
'stylesheets/global.css',
|
|
7
|
-
];
|
|
8
|
-
const componentStylesheetViewNames = viewNames.filter(viewName => viewName.match(/\.css$/) && !nonComponentStylesheetViewNames.includes(viewName))
|
|
9
|
-
return [ ...nonComponentStylesheetViewNames, ...componentStylesheetViewNames ];
|
|
10
|
-
};
|