@storecraft/database-sql-base 1.0.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 (49) hide show
  1. package/README.md +126 -0
  2. package/TODO.md +2 -0
  3. package/db-strategy.md +3 -0
  4. package/driver.js +190 -0
  5. package/index.js +3 -0
  6. package/migrate.js +66 -0
  7. package/migrations.mssql/00000_init_tables.js +268 -0
  8. package/migrations.mysql/00000_init_tables.js +372 -0
  9. package/migrations.mysql/00001_seed_email_templates.js +1 -0
  10. package/migrations.postgres/00000_init_tables.js +358 -0
  11. package/migrations.postgres/00001_seed_email_templates.js +1 -0
  12. package/migrations.shared/00001_seed_email_templates.js +260 -0
  13. package/migrations.sqlite/00000_init_tables.js +357 -0
  14. package/migrations.sqlite/00001_seed_email_templates.js +1 -0
  15. package/package.json +47 -0
  16. package/src/con.auth_users.js +159 -0
  17. package/src/con.collections.js +197 -0
  18. package/src/con.customers.js +202 -0
  19. package/src/con.discounts.js +225 -0
  20. package/src/con.discounts.utils.js +180 -0
  21. package/src/con.helpers.json.js +231 -0
  22. package/src/con.helpers.json.mssql.js +233 -0
  23. package/src/con.helpers.json.mysql.js +239 -0
  24. package/src/con.helpers.json.postgres.js +223 -0
  25. package/src/con.helpers.json.sqlite.js +263 -0
  26. package/src/con.images.js +230 -0
  27. package/src/con.notifications.js +149 -0
  28. package/src/con.orders.js +156 -0
  29. package/src/con.posts.js +147 -0
  30. package/src/con.products.js +497 -0
  31. package/src/con.search.js +148 -0
  32. package/src/con.shared.js +616 -0
  33. package/src/con.shipping.js +147 -0
  34. package/src/con.storefronts.js +301 -0
  35. package/src/con.tags.js +120 -0
  36. package/src/con.templates.js +133 -0
  37. package/src/kysely.sanitize.plugin.js +40 -0
  38. package/src/utils.funcs.js +77 -0
  39. package/src/utils.query.js +195 -0
  40. package/tests/query.cursor.test.js +389 -0
  41. package/tests/query.vql.test.js +71 -0
  42. package/tests/runner.mssql-local.test.js +118 -0
  43. package/tests/runner.mysql-local.test.js +101 -0
  44. package/tests/runner.postgres-local.test.js +99 -0
  45. package/tests/runner.sqlite-local.test.js +99 -0
  46. package/tests/sandbox.test.js +71 -0
  47. package/tsconfig.json +21 -0
  48. package/types.public.d.ts +19 -0
  49. package/types.sql.tables.d.ts +247 -0
@@ -0,0 +1,357 @@
1
+ import { CreateTableBuilder, Kysely } from 'kysely'
2
+
3
+ /**
4
+ * @typedef {import('../types.sql.tables.js').Database} Database
5
+ */
6
+
7
+ /**
8
+ * @template {string} TB
9
+ * @template {string} B
10
+ * @param {CreateTableBuilder<TB, B>} tb
11
+ */
12
+ const add_base_columns = tb => {
13
+ return tb
14
+ .addColumn('id', 'text', (col) =>
15
+ col.primaryKey()
16
+ )
17
+ .addColumn('handle', 'text', (col) => col.unique())
18
+ .addColumn('created_at', 'text')
19
+ .addColumn('updated_at', 'text')
20
+ .addColumn('attributes', 'json')
21
+ .addColumn('description', 'text')
22
+ .addColumn('active', 'integer')
23
+ }
24
+
25
+ /**
26
+ * @param {Kysely<Database>} db
27
+ * @param {keyof Database} table_name
28
+ */
29
+ const create_entity_to_value_table = (db, table_name) => {
30
+ return db.schema
31
+ .createTable(table_name)
32
+ .addColumn('id', 'integer',
33
+ (col) => col.autoIncrement().primaryKey()
34
+ )
35
+ .addColumn('entity_id', 'text', col => col.notNull())
36
+ .addColumn('entity_handle', 'text')
37
+ .addColumn('value', 'text')
38
+ .addColumn('reporter', 'text')
39
+ .addColumn('context', 'text')
40
+ }
41
+
42
+ /**
43
+ *
44
+ * @param {Kysely<Database>} db
45
+ * @param {keyof Database} table_name
46
+ */
47
+ const create_safe_table = (db, table_name) => {
48
+ return db.schema.createTable(table_name);
49
+ }
50
+
51
+ /**
52
+ * @param {Kysely<Database>} db
53
+ * @param {keyof Database} table_name
54
+ */
55
+ const drop_safe_table = (db, table_name) => {
56
+ return db.schema.dropTable(table_name).execute();
57
+ }
58
+
59
+ /**
60
+ * @param {Kysely<Database>} db
61
+ * @param {keyof Database} table_name
62
+ * @param {boolean} [include_id=true]
63
+ * @param {boolean} [include_handle=true]
64
+ */
65
+ const create_base_indexes = async (db, table_name, include_id=true, include_handle=true) => {
66
+ if(include_id) {
67
+ await db.schema.createIndex(`index_${table_name}_id_updated_at_asc`)
68
+ .on(table_name)
69
+ .columns(['id', 'updated_at asc'])
70
+ .execute();
71
+ await db.schema.createIndex(`index_${table_name}_id_updated_at_desc`)
72
+ .on(table_name)
73
+ .columns(['id', 'updated_at desc'])
74
+ .execute();
75
+ }
76
+
77
+ if(include_handle) {
78
+ await db.schema.createIndex(`index_${table_name}_handle_updated_at_asc`)
79
+ .on(table_name)
80
+ .columns(['handle', 'updated_at asc'])
81
+ .execute();
82
+ await db.schema.createIndex(`index_${table_name}_handle_updated_at_desc`)
83
+ .on(table_name)
84
+ .columns(['handle', 'updated_at desc'])
85
+ .execute();
86
+ }
87
+ }
88
+
89
+
90
+ /**
91
+ * @param {Kysely<Database>} db
92
+ * @param {keyof Pick<Database, 'entity_to_media' |
93
+ * 'entity_to_search_terms' | 'entity_to_tags_projections' |
94
+ * 'products_to_collections' | 'products_to_discounts' |
95
+ * 'products_to_variants' | 'products_to_related_products' | 'storefronts_to_other'>} table_name
96
+ */
97
+ const create_entity_table_indexes = async (db, table_name) => {
98
+ await db.schema.createIndex(`index_${table_name}_entity_id`)
99
+ .on(table_name)
100
+ .column('entity_id')
101
+ .execute();
102
+ await db.schema.createIndex(`index_${table_name}_entity_handle`)
103
+ .on(table_name)
104
+ .column('entity_handle')
105
+ .execute();
106
+ await db.schema.createIndex(`index_${table_name}_value`)
107
+ .on(table_name)
108
+ .column('value')
109
+ .execute();
110
+ await db.schema.createIndex(`index_${table_name}_reporter`)
111
+ .on(table_name)
112
+ .column('reporter')
113
+ .execute();
114
+ await db.schema.createIndex(`index_${table_name}_context`)
115
+ .on(table_name)
116
+ .column('context')
117
+ .execute();
118
+ }
119
+
120
+ /**
121
+ *
122
+ * @param {Kysely<Database>} db
123
+ */
124
+ export async function up(db) {
125
+ { // auth_users
126
+ let tb = create_safe_table(db, 'auth_users');
127
+ tb = add_base_columns(tb);
128
+ tb = tb
129
+ .addColumn('email', 'text', (col) => col.unique())
130
+ .addColumn('password', 'text')
131
+ .addColumn('roles', 'json')
132
+ .addColumn('confirmed_mail', 'integer');
133
+ await tb.execute();
134
+ await create_base_indexes(db, 'auth_users');
135
+ }
136
+
137
+ { // tags
138
+ let tb = create_safe_table(db, 'tags');
139
+ tb = add_base_columns(tb);
140
+ tb = tb.addColumn('values', 'json');
141
+ await tb.execute();
142
+ await create_base_indexes(db, 'tags');
143
+ }
144
+
145
+ { // templates
146
+ let tb = create_safe_table(db, 'templates');
147
+ tb = add_base_columns(tb);
148
+ tb = tb.addColumn('title', 'text');
149
+ tb = tb.addColumn('template_html', 'text');
150
+ tb = tb.addColumn('template_text', 'text');
151
+ tb = tb.addColumn('reference_example_input', 'json');
152
+ await tb.execute();
153
+ await create_base_indexes(db, 'templates');
154
+ }
155
+
156
+ { // collections
157
+ let tb = create_safe_table(db, 'collections');
158
+ tb = add_base_columns(tb);
159
+ tb = tb
160
+ .addColumn('title', 'text')
161
+ .addColumn('published', 'text')
162
+ await tb.execute();
163
+ await create_base_indexes(db, 'collections');
164
+ }
165
+
166
+ { // products
167
+ let tb = create_safe_table(db, 'products');
168
+ tb = add_base_columns(tb);
169
+ tb = tb
170
+ .addColumn('title', 'text')
171
+ .addColumn('video', 'text')
172
+ .addColumn('price', 'numeric')
173
+ .addColumn('isbn', 'text', (col) => col.unique())
174
+ .addColumn('compare_at_price', 'numeric')
175
+ .addColumn('qty', 'integer')
176
+ .addColumn('variants_options', 'json')
177
+ .addColumn('parent_handle', 'text')
178
+ .addColumn('parent_id', 'text')
179
+ .addColumn('variant_hint', 'json')
180
+ await tb.execute();
181
+ await create_base_indexes(db, 'products');
182
+ }
183
+
184
+ { // products_to_collections
185
+ await create_entity_to_value_table(db, 'products_to_collections').execute();
186
+ await create_entity_table_indexes(db, 'products_to_collections');
187
+ }
188
+
189
+ { // products_to_discounts
190
+ await create_entity_to_value_table(db, 'products_to_discounts').execute();
191
+ await create_entity_table_indexes(db, 'products_to_discounts');
192
+ }
193
+
194
+ { // products_to_variants
195
+ await create_entity_to_value_table(db, 'products_to_variants').execute();
196
+ await create_entity_table_indexes(db, 'products_to_variants');
197
+ }
198
+
199
+ { // products_to_related_products
200
+ await create_entity_to_value_table(db, 'products_to_related_products').execute();
201
+ await create_entity_table_indexes(db, 'products_to_related_products');
202
+ }
203
+
204
+ { // shipping_methods
205
+ let tb = create_safe_table(db, 'shipping_methods');
206
+ tb = add_base_columns(tb);
207
+ tb = tb.addColumn('title', 'text')
208
+ .addColumn('price', 'numeric')
209
+ await tb.execute();
210
+ await create_base_indexes(db, 'shipping_methods');
211
+ }
212
+
213
+ { // posts
214
+ let tb = create_safe_table(db, 'posts');
215
+ tb = add_base_columns(tb);
216
+ tb = tb
217
+ .addColumn('title', 'text')
218
+ .addColumn('text', 'text')
219
+ await tb.execute();
220
+ await create_base_indexes(db, 'posts');
221
+ }
222
+
223
+ { // customers
224
+ let tb = create_safe_table(db, 'customers');
225
+ tb = add_base_columns(tb);
226
+ tb = tb
227
+ .addColumn('email', 'text', (col) => col.unique())
228
+ .addColumn('auth_id', 'text', (col) => col.unique())
229
+ .addColumn('firstname', 'text')
230
+ .addColumn('lastname', 'text')
231
+ .addColumn('phone_number', 'text')
232
+ .addColumn('address', 'json')
233
+ await tb.execute();
234
+ await create_base_indexes(db, 'customers');
235
+ }
236
+
237
+ { // orders
238
+ let tb = create_safe_table(db, 'orders');
239
+ tb = add_base_columns(tb);
240
+ tb = tb
241
+ .addColumn('contact', 'json')
242
+ .addColumn('address', 'json')
243
+ .addColumn('line_items', 'json')
244
+ .addColumn('notes', 'text')
245
+ .addColumn('shipping_method', 'json')
246
+ .addColumn('status', 'json')
247
+ .addColumn('pricing', 'json')
248
+ .addColumn('validation', 'json')
249
+ .addColumn('payment_gateway', 'json')
250
+ .addColumn('coupons', 'json')
251
+ .addColumn('_customer_id', 'text')
252
+ .addColumn('_customer_email', 'text')
253
+ .addColumn('_status_payment_id', 'integer')
254
+ .addColumn('_status_checkout_id', 'integer')
255
+ .addColumn('_status_fulfillment_id', 'integer')
256
+
257
+ await tb.execute();
258
+ await create_base_indexes(db, 'orders');
259
+ }
260
+
261
+ { // storefronts
262
+ let tb = create_safe_table(db, 'storefronts');
263
+ tb = add_base_columns(tb);
264
+ tb = tb
265
+ .addColumn('title', 'text')
266
+ .addColumn('video', 'text')
267
+ .addColumn('published', 'text')
268
+ await tb.execute();
269
+ await create_base_indexes(db, 'storefronts');
270
+ }
271
+
272
+ { // storefronts_to_other
273
+ await create_entity_to_value_table(db, 'storefronts_to_other').execute();
274
+ await create_entity_table_indexes(db, 'storefronts_to_other');
275
+ }
276
+
277
+ { // notifications
278
+ let tb = create_safe_table(db, 'notifications');
279
+ tb = add_base_columns(tb);
280
+ tb = tb
281
+ .addColumn('message', 'text')
282
+ .addColumn('author', 'text')
283
+ .addColumn('actions', 'json')
284
+ // .addColumn('search', 'json')
285
+ await tb.execute();
286
+ await create_base_indexes(db, 'notifications', true, false);
287
+ }
288
+
289
+ { // images
290
+ let tb = create_safe_table(db, 'images');
291
+ tb = add_base_columns(tb);
292
+ tb = tb
293
+ .addColumn('name', 'text')
294
+ .addColumn('url', 'text')
295
+ await tb.execute();
296
+ await create_base_indexes(db, 'images');
297
+ }
298
+
299
+ { // discounts
300
+ let tb = create_safe_table(db, 'discounts');
301
+ tb = add_base_columns(tb);
302
+ tb = tb
303
+ .addColumn('title', 'text')
304
+ .addColumn('published', 'text')
305
+ .addColumn('priority', 'integer')
306
+ .addColumn('info', 'json')
307
+ .addColumn('application', 'json')
308
+ .addColumn('_application_id', 'integer')
309
+ .addColumn('_discount_type_id', 'integer')
310
+ await tb.execute();
311
+ await create_base_indexes(db, 'discounts');
312
+ }
313
+
314
+ { // entity_to_tags_projections
315
+ await create_entity_to_value_table(db, 'entity_to_tags_projections').execute();
316
+ await create_entity_table_indexes(db, 'entity_to_tags_projections');
317
+ }
318
+
319
+ { // entity_to_search_terms
320
+ await create_entity_to_value_table(db, 'entity_to_search_terms').execute();
321
+ await create_entity_table_indexes(db, 'entity_to_search_terms');
322
+ }
323
+
324
+ { // entity_to_media
325
+ await create_entity_to_value_table(db, 'entity_to_media').execute();
326
+ await create_entity_table_indexes(db, 'entity_to_media');
327
+ }
328
+
329
+ }
330
+
331
+ /**
332
+ *
333
+ * @param {Kysely<Database>} db
334
+ */
335
+ export async function down(db) {
336
+ await Promise.all([
337
+ drop_safe_table(db, 'auth_users'),
338
+ drop_safe_table(db, 'tags'),
339
+ drop_safe_table(db, 'collections'),
340
+ drop_safe_table(db, 'customers'),
341
+ drop_safe_table(db, 'discounts'),
342
+ drop_safe_table(db, 'images'),
343
+ drop_safe_table(db, 'notifications'),
344
+ drop_safe_table(db, 'orders'),
345
+ drop_safe_table(db, 'posts'),
346
+ drop_safe_table(db, 'shipping_methods'),
347
+ drop_safe_table(db, 'products'),
348
+ drop_safe_table(db, 'products_to_collections'),
349
+ drop_safe_table(db, 'products_to_discounts'),
350
+ drop_safe_table(db, 'products_to_variants'),
351
+ drop_safe_table(db, 'storefronts'),
352
+ drop_safe_table(db, 'storefronts_to_other'),
353
+ drop_safe_table(db, 'entity_to_media'),
354
+ drop_safe_table(db, 'entity_to_search_terms'),
355
+ drop_safe_table(db, 'entity_to_tags_projections'),
356
+ ]);
357
+ }
@@ -0,0 +1 @@
1
+ export * from '../migrations.shared/00001_seed_email_templates.js'
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@storecraft/database-sql-base",
3
+ "version": "1.0.0",
4
+ "description": "Official SQL Database driver for storecraft",
5
+ "license": "MIT",
6
+ "author": "Tomer Shalev (https://github.com/store-craft)",
7
+ "homepage": "https://github.com/store-craft/storecraft",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/store-craft/storecraft.git",
11
+ "directory": "packages/database-sql-base"
12
+ },
13
+ "keywords": [
14
+ "commerce",
15
+ "dashboard",
16
+ "code",
17
+ "storecraft"
18
+ ],
19
+ "type": "module",
20
+ "main": "index.js",
21
+ "types": "./types.public.d.ts",
22
+ "scripts": {
23
+ "test": "uvu -c",
24
+ "database-sql-base:test:sqlite": "node ./tests/runner.sqlite-local.test.js",
25
+ "database-sql-base:test:postgres": "node ./tests/runner.postgres-local.test.js",
26
+ "database-sql-base:test:mysql": "node ./tests/runner.mysql-local.test.js",
27
+ "database-sql-base:publish": "npm publish --access public"
28
+ },
29
+ "dependencies": {
30
+ "@storecraft/core": "^1.0.0",
31
+ "kysely": "^0.27.2"
32
+ },
33
+ "devDependencies": {
34
+ "@storecraft/platform-node": "^1.0.0",
35
+ "@storecraft/test-runner": "^1.0.0",
36
+ "@types/better-sqlite3": "^7.6.9",
37
+ "@types/node": "^20.11.0",
38
+ "@types/pg": "^8.11.2",
39
+ "better-sqlite3": "^9.4.3",
40
+ "dotenv": "^16.3.1",
41
+ "mysql2": "^3.9.2",
42
+ "pg": "^8.11.3",
43
+ "tarn": "^3.0.2",
44
+ "tedious": "^18.1.0",
45
+ "uvu": "^0.5.6"
46
+ }
47
+ }
@@ -0,0 +1,159 @@
1
+ import { SQL } from '../driver.js'
2
+ import { sanitize_array } from './utils.funcs.js'
3
+ import { count_regular, delete_me, insert_search_of, insert_tags_of, regular_upsert_me,
4
+ where_id_or_handle_table,
5
+ with_media,
6
+ with_tags} from './con.shared.js'
7
+ import { query_to_eb, query_to_sort } from './utils.query.js';
8
+
9
+ /**
10
+ * @typedef {import('@storecraft/core/v-database').db_auth_users} db_col
11
+ */
12
+
13
+ export const table_name = 'auth_users';
14
+
15
+ /**
16
+ * @param {SQL} driver
17
+ * @returns {db_col["upsert"]}
18
+ */
19
+ const upsert = (driver) => {
20
+ return async (item, search_terms=[]) => {
21
+ const c = driver.client;
22
+ try {
23
+ const t = await c.transaction().execute(
24
+ async (trx) => {
25
+
26
+ await insert_tags_of(trx, item.tags, item.id, item.email, table_name);
27
+ await insert_search_of(trx, search_terms, item.id, item.email, table_name);
28
+
29
+ return await regular_upsert_me(trx, table_name, {
30
+ confirmed_mail: item.confirmed_mail ? 1 : 0,
31
+ email: item.email,
32
+ handle: item.email,
33
+ password: item.password,
34
+ created_at: item.created_at,
35
+ updated_at: item.updated_at,
36
+ id: item.id,
37
+ roles: JSON.stringify(item.roles),
38
+ });
39
+ }
40
+ );
41
+ return t.numInsertedOrUpdatedRows>0;
42
+ } catch(e) {
43
+ console.log(e);
44
+ return false;
45
+ }
46
+ return true;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param {SQL} driver
52
+ * @returns {db_col["get"]}
53
+ */
54
+ const get = (driver) => {
55
+ return (id_or_email, options) => {
56
+ return driver.client
57
+ .selectFrom(table_name)
58
+ .selectAll()
59
+ .where(where_id_or_handle_table(id_or_email))
60
+ .executeTakeFirst();
61
+ }
62
+ }
63
+
64
+
65
+ /**
66
+ * @param {SQL} driver
67
+ * @returns {db_col["getByEmail"]}
68
+ */
69
+ const getByEmail = (driver) => {
70
+ return (email) => {
71
+ return driver.client
72
+ .selectFrom('auth_users')
73
+ .selectAll().where('email', '=', email)
74
+ .executeTakeFirst();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @param {SQL} driver
80
+ * @returns {db_col["remove"]}
81
+ */
82
+ const remove = (driver) => {
83
+ return async (id_or_email) => {
84
+ try {
85
+ await driver.client.transaction().execute(
86
+ async (trx) => {
87
+
88
+ // entities
89
+ // delete me
90
+ await delete_me(trx, table_name, id_or_email);
91
+ }
92
+ );
93
+ } catch(e) {
94
+ console.log(e);
95
+ return false;
96
+ }
97
+ return true;
98
+ }
99
+ }
100
+ /**
101
+ * @param {SQL} driver
102
+ * @returns {db_col["removeByEmail"]}
103
+ */
104
+ const removeByEmail = (driver) => {
105
+ return async (email) => {
106
+ const r = await driver.client
107
+ .deleteFrom('auth_users')
108
+ .where('email', '=', email)
109
+ .executeTakeFirst();
110
+ return r.numDeletedRows>0;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {SQL} driver
116
+ *
117
+ *
118
+ * @returns {db_col["list"]}
119
+ */
120
+ const list = (driver) => {
121
+ return async (query) => {
122
+
123
+ const items = await driver.client.selectFrom(table_name)
124
+ .selectAll()
125
+ .select(eb => [
126
+ with_tags(eb, eb.ref('auth_users.id'), driver.dialectType),
127
+ with_media(eb, eb.ref('auth_users.id'), driver.dialectType),
128
+ ])
129
+ .where(
130
+ (eb) => {
131
+ return query_to_eb(eb, query, table_name);
132
+ }
133
+ )
134
+ .orderBy(query_to_sort(query))
135
+ .limit(query.limitToLast ?? query.limit ?? 10)
136
+ .execute();
137
+
138
+ if(query.limitToLast) items.reverse();
139
+
140
+ return items;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * @param {SQL} driver
146
+ * @return {db_col}}
147
+ * */
148
+ export const impl = (driver) => {
149
+
150
+ return {
151
+ get: get(driver),
152
+ getByEmail: getByEmail(driver),
153
+ upsert: upsert(driver),
154
+ remove: remove(driver),
155
+ removeByEmail: removeByEmail(driver),
156
+ list: list(driver),
157
+ count: count_regular(driver, table_name),
158
+ }
159
+ }