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.
Files changed (52) hide show
  1. package/cli.js +1 -1
  2. package/lib/command.js +12 -0
  3. package/lib/commands/generate_static_site.js +8 -8
  4. package/lib/commands/generate_view.js +4 -3
  5. package/lib/commands/purge_old_sessions.js +12 -0
  6. package/lib/commands/start_server.js +10 -8
  7. package/lib/database/adapters/mysql.js +33 -2
  8. package/lib/database/adapters/sqlite.js +36 -5
  9. package/lib/database/row.js +3 -2
  10. package/lib/database.js +12 -1
  11. package/lib/extensions/_importer.js +2 -0
  12. package/lib/extensions/multi-tenant/database/table.js +18 -0
  13. package/lib/extensions/multi-tenant/database.js +6 -0
  14. package/lib/extensions/multi-tenant/index.js +4 -0
  15. package/lib/extensions/multi-tenant/migrations/1627976174_create_tenant_table_and_add_tenant_id_to_existing_tables.js +16 -0
  16. package/lib/extensions/multi-tenant/migrations/_importer.js +2 -0
  17. package/lib/extensions/multi-tenant/models/_importer.js +2 -0
  18. package/lib/extensions/multi-tenant/models/tenant.js +7 -0
  19. package/lib/extensions/multi-tenant/services/_importer.js +2 -0
  20. package/lib/extensions/multi-tenant/services/tenant.js +7 -0
  21. package/lib/import_all.js +1 -0
  22. package/lib/migrations/1628057822_create_session.js +1 -0
  23. package/lib/services/app.js +2 -0
  24. package/lib/services/bot.js +69 -0
  25. package/lib/services/fetch.js +4 -5
  26. package/lib/services/params.js +6 -1
  27. package/lib/services/session.js +8 -2
  28. package/lib/services/stylesheets.js +16 -0
  29. package/lib/url.js +2 -2
  30. package/lib/views/{_layout.js → main/_layout.js} +2 -2
  31. package/lib/views/{blocks → main/blocks}/default.js +5 -7
  32. package/lib/views/{blocks → main/blocks}/help.js +0 -0
  33. package/lib/views/{bundle.js.js → main/bundle.js.js} +1 -1
  34. package/lib/views/{sign_in.js → main/sign_in.js} +2 -1
  35. package/lib/views/{sign_out.js → main/sign_out.js} +0 -0
  36. package/lib/views/{stylesheets → main/stylesheets}/components/button.css.js +0 -0
  37. package/lib/views/{stylesheets → main/stylesheets}/components/card.css.js +0 -0
  38. package/lib/views/{stylesheets → main/stylesheets}/components/form.css.js +0 -0
  39. package/lib/views/{stylesheets → main/stylesheets}/components/frame.css.js +0 -0
  40. package/lib/views/{stylesheets → main/stylesheets}/components/input.css.js +0 -0
  41. package/lib/views/{stylesheets → main/stylesheets}/components/label.css.js +0 -0
  42. package/lib/views/{stylesheets → main/stylesheets}/components/markdown_editor.css.js +0 -0
  43. package/lib/views/{stylesheets → main/stylesheets}/components/modal.css.js +0 -0
  44. package/lib/views/{stylesheets → main/stylesheets}/components/overlay.css.js +0 -0
  45. package/lib/views/{stylesheets → main/stylesheets}/components/pagination.css.js +0 -0
  46. package/lib/views/{stylesheets → main/stylesheets}/components/progress_bar.css.js +0 -0
  47. package/lib/views/{stylesheets → main/stylesheets}/components/textarea.css.js +0 -0
  48. package/lib/views/{stylesheets → main/stylesheets}/global.css +0 -0
  49. package/lib/views/{stylesheets → main/stylesheets}/reset.css +0 -0
  50. package/lib/views/{stylesheets → main/stylesheets}/vars.css +0 -0
  51. package/package.json +6 -5
  52. package/lib/services/stylesheet_view_names.js +0 -10
package/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node --unhandled-rejections=strict
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from 'child_process';
4
4
 
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 paths = Object.keys(View.classes).filter(path => !path.match(/(^|\/)_/)).map(path => {
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(paths.length){
16
- await this.crawlPage({ _path: paths.shift() });
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._path;
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({ _path: path }))[2];
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, _path: url.path });
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, 'http://localhost/');
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}.tpl.js`;
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(() => {
@@ -0,0 +1,12 @@
1
+
2
+ export default {
3
+ meta(){
4
+ this.schedule('*/5 * * * *');
5
+ },
6
+
7
+ minutesUntilExpiry: 30,
8
+
9
+ async run(){
10
+ await this.database.withoutMultiTenancy.sessions.lastAccessedAtLt(Date.now() - (1000 * 60 * this.minutesUntilExpiry)).delete();
11
+ }
12
+ };
@@ -1,10 +1,9 @@
1
1
 
2
2
  import http from 'http';
3
3
  import Busboy from 'busboy';
4
- import { default as qs} from 'qs';
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 matches = url.match(/^([^\?]+)\?(.*)$/);
35
- const path = matches ? matches[1] : url;
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
- ...queryString,
42
+ ..._url.params,
41
43
  ...body,
42
44
  _method: method,
43
- _path: path,
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')){
@@ -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 "Id fields can't be set directly on a row";
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,2 @@
1
+
2
+ export default undefined;
@@ -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,6 @@
1
+
2
+ import { Database } from '../../database.js';
3
+
4
+ Database.include({
5
+ isMultiTenant: true
6
+ });
@@ -0,0 +1,4 @@
1
+
2
+ import { importAll } from 'pinstripe';
3
+
4
+ importAll(import.meta.url);
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { migrationImporter as default } from 'pinstripe';
@@ -0,0 +1,2 @@
1
+
2
+ export { modelImporter as default } from 'pinstripe';
@@ -0,0 +1,7 @@
1
+
2
+ export default {
3
+ meta(){
4
+ this.mustNotBeBlank('name');
5
+ this.mustNotBeBlank('host');
6
+ }
7
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { serviceImporter as default } from 'pinstripe';
@@ -0,0 +1,7 @@
1
+
2
+ export default ({ params, tenants }) => {
3
+ if(params._headers?.['x-tenant']){
4
+ return tenants.nameEq(params._headers['x-tenant']).first();
5
+ }
6
+ return tenants.hostEq(params._url.host).first();
7
+ };
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 {
@@ -3,5 +3,6 @@ export default async ({ sessions }) => {
3
3
 
4
4
  await sessions.addColumn('passString', 'string');
5
5
  await sessions.addColumn('userId', 'foreign_key');
6
+ await sessions.addColumn('lastAccessedAt', 'datetime', { index: true });
6
7
 
7
8
  };
@@ -0,0 +1,2 @@
1
+
2
+ export default async ({ params: { _headers = {} } }) => _headers['x-app'] || 'main';
@@ -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);
@@ -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._path.substr(`/${prefixSegments.join('/')}`.length).replace(/^\//, '')
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._path){
83
- out._path = '/';
81
+ if(!params._url){
82
+ out._url = Url.new();
84
83
  }
85
84
  if(!params._headers){
86
85
  out._headers = {};
@@ -1,2 +1,7 @@
1
1
 
2
- export default () => ({});
2
+ import { Url } from '../url.js';
3
+
4
+ export default () => ({
5
+ _url: Url.new(),
6
+ _headers: {}
7
+ });
@@ -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
- return database.sessions.idEq(sessionId).passStringEq(passString).first();
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 || window.location, new Url())
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, stylesheetViewNames }) => 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
- ${stylesheetViewNames.map(viewName => renderHtml`
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
- import { View } from '../../view.js';
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
- export default ({ renderHtml, params: { _path } }) => {
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
@@ -2,7 +2,7 @@
2
2
  import { Volume as MemFs } from 'memfs';
3
3
  import webpack from 'webpack';
4
4
 
5
- import { client } from '../client.js';
5
+ import { client } from '../../client.js';
6
6
 
7
7
  let cache;
8
8
 
@@ -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
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.14.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": "^27.5.1",
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": "9ccfe82e9630b92f21add4ad72963faf8c8135c3"
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
- };