pinstripe 0.31.1 → 0.32.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 (45) hide show
  1. package/lib/background_job.js +42 -0
  2. package/lib/background_jobs/_file_importer.js +1 -0
  3. package/lib/{commands → background_jobs}/purge_used_hashes.js +1 -3
  4. package/lib/command.js +0 -12
  5. package/lib/commands/generate_background_job.js +47 -0
  6. package/lib/commands/generate_project.js +1 -1
  7. package/lib/commands/list_background_jobs.js +15 -0
  8. package/lib/commands/run_background_job.js +11 -0
  9. package/lib/component.js +7 -4
  10. package/lib/components/helpers.js +18 -6
  11. package/lib/components/pinstripe_document.js +6 -2
  12. package/lib/components/pinstripe_frame.js +3 -3
  13. package/lib/components/pinstripe_modal.js +26 -7
  14. package/lib/database/client.js +65 -0
  15. package/lib/database/constants.js +13 -1
  16. package/lib/database/row.js +18 -23
  17. package/lib/database/table.js +107 -107
  18. package/lib/database.js +10 -2
  19. package/lib/defer.js +10 -10
  20. package/lib/defer.test.js +11 -0
  21. package/lib/extensions/multi-tenant/database/row.js +22 -0
  22. package/lib/extensions/multi-tenant/models/_file_importer.js +1 -0
  23. package/lib/extensions/multi-tenant/models/tenant.js +14 -0
  24. package/lib/extensions/multi-tenant/services/database.js +4 -1
  25. package/lib/extensions/multi-tenant/services/run_background_job.js +27 -0
  26. package/lib/index.js +1 -0
  27. package/lib/inflector.js +4 -0
  28. package/lib/markdown.js +13 -5
  29. package/lib/registry.js +2 -3
  30. package/lib/services/bot.js +9 -9
  31. package/lib/services/render_form.js +10 -5
  32. package/lib/services/render_table.js +48 -0
  33. package/lib/services/run_background_job.js +8 -0
  34. package/lib/views/docs/docs/guides/introduction.md +1 -1
  35. package/lib/views/shared/_button.js +51 -11
  36. package/lib/views/shared/_danger_area.js +78 -0
  37. package/lib/views/shared/_editable_area.js +4 -4
  38. package/lib/views/shared/_form.js +53 -8
  39. package/lib/views/shared/_pagination.js +47 -0
  40. package/lib/views/shared/_panel.js +4 -0
  41. package/lib/views/shared/_section.js +4 -0
  42. package/lib/views/shared/_shell/index.js +2 -1
  43. package/lib/views/shared/_table.js +139 -0
  44. package/package.json +3 -4
  45. package/development.db +0 -0
@@ -0,0 +1,42 @@
1
+
2
+ import { Class } from './class.js';
3
+ import { inflector } from './inflector.js';
4
+ import { Registry } from './registry.js';
5
+ import { ServiceConsumer } from './service_consumer.js';
6
+
7
+ export const BackgroundJob = Class.extend().include({
8
+ meta(){
9
+ this.assignProps({ name: 'BackgroundJob' });
10
+
11
+ this.include(Registry);
12
+ this.include(ServiceConsumer);
13
+
14
+ this.assignProps({
15
+ normalizeName(name){
16
+ return inflector.dasherize(name);
17
+ },
18
+
19
+ get schedules(){
20
+ if(!this.hasOwnProperty('_schedules')){
21
+ this._schedules = [];
22
+ }
23
+ return this._schedules;
24
+ },
25
+
26
+ schedule(...args){
27
+ this.schedules.push(args);
28
+ return this;
29
+ },
30
+
31
+ async run(context, name){
32
+ await context.fork().run(async context => {
33
+ await this.create(name, context).run();
34
+ });
35
+ },
36
+ });
37
+ },
38
+
39
+ run(){
40
+ console.error(`No such background job "${this.constructor.name}" exists.`);
41
+ }
42
+ });
@@ -0,0 +1 @@
1
+ export { BackgroundJob as default } from 'pinstripe';
@@ -4,10 +4,8 @@ export default {
4
4
  this.schedule('*/5 * * * *');
5
5
  },
6
6
 
7
- minutesUntilExpiry: 30,
8
-
9
7
  async run(){
10
- await this.database.withoutTenantScope.usedHashes.where({
8
+ await this.database.usedHashes.where({
11
9
  expiresAtLt: new Date()
12
10
  }).delete();
13
11
  }
package/lib/command.js CHANGED
@@ -16,18 +16,6 @@ export const Command = Class.extend().include({
16
16
  return inflector.dasherize(name);
17
17
  },
18
18
 
19
- get schedules(){
20
- if(!this.hasOwnProperty('_schedules')){
21
- this._schedules = [];
22
- }
23
- return this._schedules;
24
- },
25
-
26
- schedule(...args){
27
- this.schedules.push(args);
28
- return this;
29
- },
30
-
31
19
  async run(context, name = 'list-commands', ...args){
32
20
  await context.fork().run(async context => {
33
21
  context.args = [ ...args ];
@@ -0,0 +1,47 @@
1
+
2
+
3
+ export default {
4
+ async run(){
5
+ const [ name = '' ] = this.args;
6
+ const normalizedName = this.inflector.snakeify(name);
7
+ if(normalizedName == ''){
8
+ console.error('A background_job name must be given.');
9
+ process.exit();
10
+ }
11
+
12
+ const { inProjectRootDir, generateFile, line, indent } = this.fsBuilder;
13
+
14
+ await inProjectRootDir(async () => {
15
+
16
+ await generateFile(`lib/background_jobs/_file_importer.js`, { skipIfExists: true }, () => {
17
+ line();
18
+ line(`export { BackgroundJob as default } from 'pinstripe';`);
19
+ line();
20
+ });
21
+
22
+ await generateFile(`lib/background_jobs/${normalizedName}.js`, () => {
23
+ line();
24
+ line(`export default {`);
25
+ indent(() => {
26
+ line('meta(){');
27
+ indent(() => {
28
+ line(`this.schedule('* * * * *'); // run every minute`);
29
+ });
30
+ line('}');
31
+ });
32
+ line();
33
+ indent(() => {
34
+ line('run(){');
35
+ indent(() => {
36
+ line(`console.log('${this.inflector.dasherize(normalizedName)} background job coming soon!')`);
37
+ });
38
+ line('}');
39
+ });
40
+ line('};');
41
+ line();
42
+ });
43
+
44
+ });
45
+ }
46
+ }
47
+
@@ -134,7 +134,7 @@ export default {
134
134
  line();
135
135
  });
136
136
 
137
- spawnSync('yarn', [ 'add', ...dependencies ], {
137
+ spawnSync('npm', [ 'install', ...dependencies ], {
138
138
  stdio: 'inherit'
139
139
  });
140
140
  });
@@ -0,0 +1,15 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { BackgroundJob } from 'pinstripe';
4
+
5
+ export default {
6
+ run(){
7
+ console.log('');
8
+ console.log('The following background jobs are available:');
9
+ console.log('');
10
+ BackgroundJob.names.forEach(backgroundJobName => {
11
+ console.log(` * ${chalk.green(backgroundJobName)}`);
12
+ });
13
+ console.log('');
14
+ }
15
+ };
@@ -0,0 +1,11 @@
1
+
2
+ export default {
3
+ async run(){
4
+ const [ name = '' ] = this.args;
5
+ if(name == ''){
6
+ console.error('A background job name must be given.');
7
+ process.exit();
8
+ }
9
+ this.runBackgroundJob(name);
10
+ }
11
+ };
package/lib/component.js CHANGED
@@ -203,7 +203,7 @@ export const Component = Class.extend().include({
203
203
  },
204
204
 
205
205
  get isInput(){
206
- return this.is('input, textarea');
206
+ return this.is('input, textarea, select');
207
207
  },
208
208
 
209
209
  get name(){
@@ -220,6 +220,9 @@ export const Component = Class.extend().include({
220
220
  if(this.is('input[type="checkbox"]')){
221
221
  return this.is(':checked') ? true : false;
222
222
  }
223
+ if(this.is('select')){
224
+ return this.findAll('option').map(option => option.value)[this.node.selectedIndex];
225
+ }
223
226
  return this.node.value;
224
227
  },
225
228
 
@@ -597,14 +600,14 @@ function patchAttributes(attributes){
597
600
  if(attributes[key] === undefined){
598
601
  Element.prototype.removeAttribute.call(this.node, key); // work around for https://github.com/cypress-io/cypress/issues/26206
599
602
  // this.node.removeAttribute(key);
603
+ if(key == 'checked') this.node.checked = false;
600
604
  }
601
605
  })
602
606
  Object.keys(attributes).forEach((key) => {
603
607
  if(!currentAttributes.hasOwnProperty(key) || currentAttributes[key] != attributes[key]){
604
608
  this.node.setAttribute(key, attributes[key]);
605
- if(key == 'value'){
606
- this.node.value = attributes[key];
607
- }
609
+ if(key == 'value') this.node.value = attributes[key];
610
+ if(key == 'checked') this.node.checked = true;
608
611
  }
609
612
  })
610
613
  }
@@ -51,11 +51,23 @@ export function normalizeUrl(url, referenceUrl = window.location){
51
51
  const matches = `${url}`.match(/^&(.*)$/);
52
52
  const out = matches ? new URL(referenceUrl) : new URL(url, referenceUrl);
53
53
  if(matches){
54
- if(out.search){
55
- out.search = `${out.search}&${matches[1]}`;
56
- } else {
57
- out.search = `?${matches[1]}`;
58
- }
54
+ out.search = `?${stringifyUrlSearch({
55
+ ...parseUrlSearch(out.search),
56
+ ...parseUrlSearch(`${matches[1]}`)
57
+ })}`;
59
58
  }
60
59
  return out;
61
- }
60
+ }
61
+
62
+ function parseUrlSearch(search){
63
+ const out = {};
64
+ search.replace(/^\?/, '').split('&').forEach(pair => {
65
+ const [key, value] = pair.split('=');
66
+ out[decodeURIComponent(key)] = decodeURIComponent(value);
67
+ });
68
+ return out;
69
+ }
70
+
71
+ function stringifyUrlSearch(search){
72
+ return Object.keys(search).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(search[key])}`).join('&');
73
+ }
@@ -47,6 +47,10 @@ export default {
47
47
  return this.body.progressBar;
48
48
  },
49
49
 
50
+ get loadCacheNamespace(){
51
+ return this.head.find('meta[name="pinstripe-load-cache-namespace"]')?.params.content ?? 'default';
52
+ },
53
+
50
54
  async load(url = this.url.toString(), options = {}){
51
55
  const { replace, method = 'GET' } = options;
52
56
  const previousUrl = this.url.toString();
@@ -65,12 +69,12 @@ export default {
65
69
  },
66
70
 
67
71
  async preload(url){
68
- if(loadCache.get(url.toString())) return;
72
+ if(loadCache.get(`${this.document.loadCacheNamespace}:${url}`)) return;
69
73
  if(preloading[url.toString()]) return;
70
74
  preloading[url.toString()] = true;
71
75
  const response = await fetch(url);
72
76
  const html = await response.text();
73
- loadCache.put(url.toString(), html);
77
+ loadCache.put(`${this.document.loadCacheNamespace}:${url}`, html);
74
78
  delete preloading[url.toString()];
75
79
  }
76
80
  };
@@ -47,11 +47,11 @@ export default {
47
47
  this.loading = true;
48
48
  this.abort();
49
49
  const { method = 'GET', placeholderUrl } = options;
50
- const cachedHtml = method == 'GET' ? loadCache.get(url.toString()) : undefined;
50
+ const cachedHtml = method == 'GET' ? loadCache.get(`${this.document.loadCacheNamespace}:${url}`) : undefined;
51
51
  if(cachedHtml) this.patch(cachedHtml);
52
52
  let minimumDelay = 0;
53
53
  if(!cachedHtml && placeholderUrl){
54
- const placeholderHtml = loadCache.get(placeholderUrl.toString());
54
+ const placeholderHtml = loadCache.get(`${this.document.loadCacheNamespace}:${placeholderUrl}`);
55
55
  if(placeholderHtml) {
56
56
  this.patch(placeholderHtml);
57
57
  minimumDelay = 300;
@@ -62,7 +62,7 @@ export default {
62
62
  this.loading = false;
63
63
  if(html == cachedHtml && !this.loadWasBlocked) return;
64
64
  this.loadWasBlocked = false;
65
- if(method == 'GET') loadCache.put(this.url.toString(), html);
66
65
  this.patch(html);
66
+ if(method == 'GET') loadCache.put(`${this.document.loadCacheNamespace}:${this.url}`, html);
67
67
  }
68
68
  };
@@ -3,6 +3,8 @@ export default {
3
3
  initialize(...args){
4
4
  this.constructor.parent.prototype.initialize.call(this, ...args);
5
5
 
6
+ const { width = 'medium', height = 'auto' } = this.params;
7
+
6
8
  this.shadow.patch(`
7
9
  <style>
8
10
  .root {
@@ -58,20 +60,37 @@ export default {
58
60
  height: 50%;
59
61
  width: 0.2rem;
60
62
  }
63
+ .container {
64
+ min-height: calc(100vh - 4.0rem);
65
+ width: calc(100vw - 16.0rem);
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ }
61
70
  .body {
62
- max-height: calc(100vh - 4.0rem);
63
- max-width: calc(100vw - 16.0rem);
64
- min-width: 64.0rem;
65
- margin: 0 auto;
71
+ max-width: 100%;
72
+ }
73
+ .root.is-medium-width .body {
74
+ width: 800px;
75
+ }
76
+ .root.is-full-width .body {
77
+ width: 100%;
78
+ }
79
+ .root.is-full-height .body {
80
+ height: 100%;
66
81
  }
67
82
  </style>
68
- <div class="root">
83
+ <div class="root is-${width}-width is-${height}-height">
69
84
  <button class="close-button"></button>
70
- <div class="body"><slot></div>
85
+ <div class="container">
86
+ <div class="body">
87
+ <slot>
88
+ </div>
89
+ </div>
71
90
  </div>
72
91
  `);
73
92
 
74
- this.shadow.on('click', '.root, .close-button', () => this.trigger('close'));
93
+ this.shadow.on('click', '.root, .container, .body, .close-button', () => this.trigger('close'));
75
94
  },
76
95
 
77
96
  isModal: true
@@ -4,6 +4,8 @@ import { existsSync, unlinkSync } from 'fs';
4
4
 
5
5
  import { Class } from '../class.js';
6
6
 
7
+ import { MYSQL_COLUMN_TYPE_TO_TYPE_MAP, SQLITE_COLUMN_TYPE_TO_TYPE_MAP } from './constants.js';
8
+
7
9
  let sqliteConnectionCounters = {};
8
10
  let sqliteConnections = {};
9
11
 
@@ -143,6 +145,69 @@ export const Client = Class.extend().include({
143
145
  const fn = alternatives[adapter];
144
146
  if(!fn) throw new Error(`"${adapter}" adapter not supported.`);
145
147
  return fn.call(that);
148
+ },
149
+
150
+ async extractSchema(){
151
+ const out = {};
152
+
153
+ const tableNames = await this.adapt(this, {
154
+ async mysql(){
155
+ const rows = await this.run('show tables');
156
+ return rows.map(row => Object.values(row)[0]);
157
+ },
158
+
159
+ async sqlite(){
160
+ const rows = await this.run(`select name from sqlite_schema where type ='table' and name not like 'sqlite_%'`);
161
+ return rows.map(({ name }) => name);
162
+ }
163
+ });
164
+
165
+ while(tableNames.length){
166
+ const tableName = tableNames.shift();
167
+ const columns = await this.adapt(this, {
168
+ async mysql(){
169
+ const out = {};
170
+ const rows = await this.run(`describe \`${tableName}\``);
171
+ rows.forEach(row => {
172
+ const name = row['Field'];
173
+ let type;
174
+ if(name == '_id'){
175
+ type = 'primary_key';
176
+ } else if(name == 'id'){
177
+ type = 'alternate_key';
178
+ } else {
179
+ type = MYSQL_COLUMN_TYPE_TO_TYPE_MAP[row['Type']] || 'string';
180
+ }
181
+
182
+ out[name] = type;
183
+ });
184
+ return out;
185
+ },
186
+
187
+ async sqlite(){
188
+ const out = {};
189
+ const rows = await this.run(`pragma table_info(\`${tableName}\`)`);
190
+ rows.forEach(row => {
191
+ const { name } = row;
192
+ let type;
193
+ if(name == '_id'){
194
+ type = 'primary_key';
195
+ } else if(name == 'id'){
196
+ type = 'alternate_key';
197
+ } else if(name.match(/.+Id$/)){
198
+ type = 'foreign_key';
199
+ } else {
200
+ type = SQLITE_COLUMN_TYPE_TO_TYPE_MAP[row.type] || 'string';
201
+ }
202
+ out[name] = type;
203
+ });
204
+ return out;
205
+ }
206
+ });
207
+ out[tableName] = columns;
208
+ }
209
+
210
+ return out;
146
211
  }
147
212
  });
148
213
 
@@ -46,7 +46,7 @@ export const SQLITE_COLUMN_TYPE_TO_TYPE_MAP = (() => {
46
46
  return out;
47
47
  })();
48
48
 
49
- export const COMPARISON_OPERATORS = {
49
+ export const MYSQL_COMPARISON_OPERATORS = {
50
50
  '': '? = ?',
51
51
  Ne: '? != ?',
52
52
  Lt: '? < ?',
@@ -58,6 +58,18 @@ export const COMPARISON_OPERATORS = {
58
58
  Contains: `? like concat('%', ?, '%')`
59
59
  };
60
60
 
61
+ export const SQLITE_COMPARISON_OPERATORS = {
62
+ '': '? = ?',
63
+ Ne: '? != ?',
64
+ Lt: '? < ?',
65
+ Gt: '? > ?',
66
+ Le: '? <= ?',
67
+ Ge: '? >= ?',
68
+ BeginsWith: `? like (? || '%')`,
69
+ EndsWith: `? like ('%' || ?)`,
70
+ Contains: `? like ('%' || ? || '%')`
71
+ };
72
+
61
73
  export const MYSQL_KEY_COMPARISON_OPERATORS = {
62
74
  '': '? = uuid_to_bin(?)',
63
75
  Ne: '? != uuid_to_bin(?)'
@@ -18,15 +18,6 @@ export const Row = Model.extend().include({
18
18
  defineCallbacks.call(this, 'beforeInsert', 'afterInsert', 'beforeUpdate', 'afterUpdate', 'beforeDelete', 'afterDelete')
19
19
 
20
20
  this.assignProps({
21
-
22
- async loadSchema(client){
23
- await Table.loadSchema(client);
24
- this.clearCache();
25
- this.names.forEach(name => this.for(name));
26
- Table.clearCache();
27
- Table.names.forEach(name => Table.for(name));
28
- },
29
-
30
21
  normalizeName(name){
31
22
  return inflector.camelize(name);
32
23
  },
@@ -61,11 +52,7 @@ export const Row = Model.extend().include({
61
52
  includeInTable(...args){
62
53
  if(this.abstract) return;
63
54
 
64
- Table.register(this.collectionName, {
65
- meta(){
66
- this.include(...args);
67
- }
68
- });
55
+ Table.for(this.collectionName).include(...args);
69
56
  },
70
57
 
71
58
  scope(...args){
@@ -110,6 +97,16 @@ export const Row = Model.extend().include({
110
97
  cascadeDelete: false,
111
98
  ...options
112
99
  });
100
+ },
101
+
102
+ mustBeUnique(name, options = {}){
103
+ const { message = 'Must be unique', collection = this.collectionName } = options;
104
+ return this.validateWith(async row => {
105
+ if(row.isValidationError(name)) return;
106
+ const value = row[name];
107
+ const alreadyExists = await row.database[collection].where({ [name]: value, idNe: row.id }).count() > 0;
108
+ if(alreadyExists) row.setValidationError(name, message);
109
+ });
113
110
  }
114
111
  });
115
112
  },
@@ -153,13 +150,15 @@ export const Row = Model.extend().include({
153
150
  modifiedFields[name] = this[name];
154
151
  }
155
152
  });
153
+
154
+ const exists = this._exists;
156
155
 
157
156
  if(Object.keys(modifiedFields).length) {
158
157
  const query = [];
159
158
 
160
159
  const tableReference = TableReference.new(this.constructor.collectionName);
161
160
 
162
- if(this._exists){
161
+ if(exists){
163
162
  query.push('update ? set ', tableReference);
164
163
  Object.keys(modifiedFields).forEach((name, i) => {
165
164
  if(name == 'id') return;
@@ -186,7 +185,6 @@ export const Row = Model.extend().include({
186
185
  query.push(' where ? = ?', tableReference.createColumnReference('id'), this._initialFields.id);
187
186
  }
188
187
  });
189
-
190
188
  } else {
191
189
  query.push('insert into ?(', tableReference);
192
190
  Object.keys(modifiedFields).forEach((name, i) => {
@@ -227,10 +225,9 @@ export const Row = Model.extend().include({
227
225
  });
228
226
 
229
227
  this._exists = true;
230
-
231
228
  }
232
229
 
233
- await (this._exists ? this._runAfterUpdateCallbacks() : this._runAfterInsertCallbacks());
230
+ await (exists ? this._runAfterUpdateCallbacks() : this._runAfterInsertCallbacks());
234
231
 
235
232
  return this;
236
233
  });
@@ -272,10 +269,7 @@ export const Row = Model.extend().include({
272
269
  const names = [...new Set([ ...Object.keys(columns), ...extractSettableProps(this) ])];
273
270
  while(names.length){
274
271
  const name = names.shift();
275
- let value = await this[name];
276
- if(typeof value?.toFieldValue == 'function'){
277
- value = await value.toFieldValue();
278
- }
272
+ const value = await this[name];
279
273
  const type = columns[name] ? columns[name] : typeof value;
280
274
  fields.push({
281
275
  name,
@@ -300,7 +294,8 @@ export const Row = Model.extend().include({
300
294
 
301
295
  function defineRelationship({ name, type, collectionName, fromKey, toKey, cascadeDelete, through }){
302
296
  if(this.abstract) return;
303
- Table.register(this.collectionName, {
297
+
298
+ this.includeInTable({
304
299
  meta(){
305
300
  this.prototype.assignProps({
306
301
  get [name](){