@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,263 @@
1
+ import { SelectQueryNode, sql} from 'kysely'
2
+ import { extract_first_selection, getJsonObjectArgs } from './con.helpers.json.js';
3
+
4
+ /**
5
+ * A SQLite helper for aggregating a subquery into a JSON array.
6
+ *
7
+ * NOTE: This helper only works correctly if you've installed the `ParseJSONResultsPlugin`.
8
+ * Otherwise the nested selections will be returned as JSON strings.
9
+ *
10
+ * The plugin can be installed like this:
11
+ *
12
+ * ```ts
13
+ * const db = new Kysely({
14
+ * dialect: new SqliteDialect(config),
15
+ * plugins: [new ParseJSONResultsPlugin()]
16
+ * })
17
+ * ```
18
+ *
19
+ * ### Examples
20
+ *
21
+ * ```ts
22
+ * const result = await db
23
+ * .selectFrom('person')
24
+ * .select((eb) => [
25
+ * 'id',
26
+ * jsonArrayFrom(
27
+ * eb.selectFrom('pet')
28
+ * .select(['pet.id as pet_id', 'pet.name'])
29
+ * .whereRef('pet.owner_id', '=', 'person.id')
30
+ * .orderBy('pet.name')
31
+ * ).as('pets')
32
+ * ])
33
+ * .execute()
34
+ *
35
+ * result[0].id
36
+ * result[0].pets[0].pet_id
37
+ * result[0].pets[0].name
38
+ * ```
39
+ *
40
+ * The generated SQL (SQLite):
41
+ *
42
+ * ```sql
43
+ * select "id", (
44
+ * select coalesce(json_group_array(json_object(
45
+ * 'pet_id', "agg"."pet_id",
46
+ * 'name', "agg"."name"
47
+ * )), '[]') from (
48
+ * select "pet"."id" as "pet_id", "pet"."name"
49
+ * from "pet"
50
+ * where "pet"."owner_id" = "person"."id"
51
+ * order by "pet"."name"
52
+ * ) as "agg"
53
+ * ) as "pets"
54
+ * from "person"
55
+ * ```
56
+ * @template O
57
+ * @param {import('./con.helpers.json.js').SelectQueryBuilderExpression<O>} expr
58
+ * @returns {import('kysely').RawBuilder<import('kysely').Simplify<O>[]>}
59
+ */
60
+ export function sqlite_jsonArrayFrom(expr) {
61
+
62
+ return sql`(select coalesce(json_group_array(json_object(${sql.join(
63
+ getSqliteJsonObjectArgs(expr.toOperationNode(), 'agg')
64
+ )})), '[]') from ${expr} as agg)`
65
+ }
66
+
67
+
68
+ /**
69
+ * A SQLite helper for aggregating a subquery into a JSON array.
70
+ *
71
+ * NOTE: This helper only works correctly if you've installed the `ParseJSONResultsPlugin`.
72
+ * Otherwise the nested selections will be returned as JSON strings.
73
+ *
74
+ * The plugin can be installed like this:
75
+ *
76
+ * ```ts
77
+ * const db = new Kysely({
78
+ * dialect: new SqliteDialect(config),
79
+ * plugins: [new ParseJSONResultsPlugin()]
80
+ * })
81
+ * ```
82
+ *
83
+ * ### Examples
84
+ *
85
+ * ```ts
86
+ * const result = await db
87
+ * .selectFrom('person')
88
+ * .select((eb) => [
89
+ * 'id',
90
+ * stringArrayFrom(
91
+ * eb.selectFrom('pet')
92
+ * .select('pet.name')
93
+ * .whereRef('pet.owner_id', '=', 'person.id')
94
+ * .orderBy('pet.name')
95
+ * ).as('pets')
96
+ * ])
97
+ * .execute()
98
+ *
99
+ * result[0].pets = ['name1', 'name2', ....]
100
+ * ```
101
+ *
102
+ * The generated SQL (SQLite):
103
+ *
104
+ * ```sql
105
+ * select "id", (
106
+ * select coalesce(json_group_array("agg"."name"), '[]') from (
107
+ * select "pet"."name"
108
+ * from "pet"
109
+ * where "pet"."owner_id" = "person"."id"
110
+ * order by "pet"."name"
111
+ * ) as "agg"
112
+ * ) as "pets"
113
+ * from "person"
114
+ * ```
115
+ * @template O
116
+ * @param {import('./con.helpers.json.js').SelectQueryBuilderExpression<O>} expr
117
+ * @returns {import('kysely').RawBuilder<import('kysely').Simplify<O>[]>}
118
+ */
119
+ export function sqlite_stringArrayFrom(expr) {
120
+ const arg = extract_first_selection(expr, 'agg');
121
+ return sql`(select coalesce(json_group_array(${sql.join([arg])}), '[]') from ${expr} as agg)`
122
+ }
123
+
124
+ /**
125
+ * A SQLite helper for turning a subquery into a JSON object.
126
+ *
127
+ * The subquery must only return one row.
128
+ *
129
+ * NOTE: This helper only works correctly if you've installed the `ParseJSONResultsPlugin`.
130
+ * Otherwise the nested selections will be returned as JSON strings.
131
+ *
132
+ * The plugin can be installed like this:
133
+ *
134
+ * ```ts
135
+ * const db = new Kysely({
136
+ * dialect: new SqliteDialect(config),
137
+ * plugins: [new ParseJSONResultsPlugin()]
138
+ * })
139
+ * ```
140
+ *
141
+ * ### Examples
142
+ *
143
+ * ```ts
144
+ * const result = await db
145
+ * .selectFrom('person')
146
+ * .select((eb) => [
147
+ * 'id',
148
+ * jsonObjectFrom(
149
+ * eb.selectFrom('pet')
150
+ * .select(['pet.id as pet_id', 'pet.name'])
151
+ * .whereRef('pet.owner_id', '=', 'person.id')
152
+ * .where('pet.is_favorite', '=', true)
153
+ * ).as('favorite_pet')
154
+ * ])
155
+ * .execute()
156
+ *
157
+ * result[0].id
158
+ * result[0].favorite_pet.pet_id
159
+ * result[0].favorite_pet.name
160
+ * ```
161
+ *
162
+ * The generated SQL (SQLite):
163
+ *
164
+ * ```sql
165
+ * select "id", (
166
+ * select json_object(
167
+ * 'pet_id', "obj"."pet_id",
168
+ * 'name', "obj"."name"
169
+ * ) from (
170
+ * select "pet"."id" as "pet_id", "pet"."name"
171
+ * from "pet"
172
+ * where "pet"."owner_id" = "person"."id"
173
+ * and "pet"."is_favorite" = ?
174
+ * ) as obj
175
+ * ) as "favorite_pet"
176
+ * from "person";
177
+ * ```
178
+ *
179
+ * @template O
180
+ * @param {import('./con.helpers.json.js').SelectQueryBuilderExpression<O>} expr
181
+ * @returns {import('kysely').RawBuilder<import('kysely').Simplify<O> | null>}
182
+ */
183
+ export function sqlite_jsonObjectFrom(expr) {
184
+ return sql`(select json_object(${sql.join(
185
+ getSqliteJsonObjectArgs(expr.toOperationNode(), 'obj'),
186
+ )}) from ${expr} as obj)`
187
+ }
188
+
189
+ /**
190
+ * The SQLite `json_object` function.
191
+ *
192
+ * NOTE: This helper only works correctly if you've installed the `ParseJSONResultsPlugin`.
193
+ * Otherwise the nested selections will be returned as JSON strings.
194
+ *
195
+ * The plugin can be installed like this:
196
+ *
197
+ * ```ts
198
+ * const db = new Kysely({
199
+ * dialect: new SqliteDialect(config),
200
+ * plugins: [new ParseJSONResultsPlugin()]
201
+ * })
202
+ * ```
203
+ *
204
+ * ### Examples
205
+ *
206
+ * ```ts
207
+ * const result = await db
208
+ * .selectFrom('person')
209
+ * .select((eb) => [
210
+ * 'id',
211
+ * jsonBuildObject({
212
+ * first: eb.ref('first_name'),
213
+ * last: eb.ref('last_name'),
214
+ * full: sql<string>`first_name || ' ' || last_name`
215
+ * }).as('name')
216
+ * ])
217
+ * .execute()
218
+ *
219
+ * result[0].id
220
+ * result[0].name.first
221
+ * result[0].name.last
222
+ * result[0].name.full
223
+ * ```
224
+ *
225
+ * The generated SQL (SQLite):
226
+ *
227
+ * ```sql
228
+ * select "id", json_object(
229
+ * 'first', first_name,
230
+ * 'last', last_name,
231
+ * 'full', "first_name" || ' ' || "last_name"
232
+ * ) as "name"
233
+ * from "person"
234
+ * ```
235
+ */
236
+ // export function jsonBuildObject<O extends Record<string, Expression<unknown>>>(
237
+ // obj: O,
238
+ // ): RawBuilder<
239
+ // Simplify<{
240
+ // [K in keyof O]: O[K] extends Expression<infer V> ? V : never
241
+ // }>
242
+ // > {
243
+ // return sql`json_object(${sql.join(
244
+ // Object.keys(obj).flatMap((k) => [sql.lit(k), obj[k]]),
245
+ // )})`
246
+ // }
247
+
248
+
249
+ /**
250
+ *
251
+ * @param {SelectQueryNode} node
252
+ * @param {string} table
253
+ * @returns {import('kysely').Expression<unknown>[]}
254
+ */
255
+ function getSqliteJsonObjectArgs(node, table) {
256
+ try {
257
+ return getJsonObjectArgs(node, table)
258
+ } catch {
259
+ throw new Error(
260
+ 'SQLite jsonArrayFrom and jsonObjectFrom functions can only handle explicit selections due to limitations of the json_object function. selectAll() is not allowed in the subquery.',
261
+ )
262
+ }
263
+ }
@@ -0,0 +1,230 @@
1
+ import { func } from '@storecraft/core/v-api'
2
+ import { SQL } from '../driver.js'
3
+ import { count_regular, delete_me, delete_search_of,
4
+ insert_search_of, regular_upsert_me, where_id_or_handle_table
5
+ } from './con.shared.js'
6
+ import { sanitize_array } from './utils.funcs.js'
7
+ import { query_to_eb, query_to_sort } from './utils.query.js'
8
+ import { Transaction } from 'kysely'
9
+ import { ID } from '@storecraft/core/v-api/utils.func.js'
10
+ import {
11
+ image_url_to_handle, image_url_to_name
12
+ } from '@storecraft/core/v-api/con.images.logic.js'
13
+
14
+ /**
15
+ * @typedef {import('@storecraft/core/v-database').db_images} db_col
16
+ */
17
+ export const table_name = 'images'
18
+
19
+ /**
20
+ * @param {SQL} driver
21
+ * @returns {db_col["upsert"]}
22
+ */
23
+ const upsert = (driver) => {
24
+ return async (item, search_terms) => {
25
+ const c = driver.client;
26
+ try {
27
+ const t = await c.transaction().execute(
28
+ async (trx) => {
29
+ await insert_search_of(
30
+ trx, search_terms, item.id, item.handle, table_name
31
+ );
32
+ await regular_upsert_me(trx, table_name, {
33
+ created_at: item.created_at,
34
+ updated_at: item.updated_at,
35
+ id: item.id,
36
+ handle: item.handle,
37
+ name: item.name,
38
+ url: item.url,
39
+ });
40
+ }
41
+ );
42
+ } catch(e) {
43
+ console.log(e);
44
+ return false;
45
+ }
46
+ return true;
47
+ }
48
+ }
49
+
50
+
51
+ /**
52
+ * @param {SQL} driver
53
+ * @returns {db_col["get"]}
54
+ */
55
+ const get = (driver) => {
56
+ return (id_or_handle, options) => {
57
+ return driver.client
58
+ .selectFrom(table_name)
59
+ .selectAll()
60
+ .where(where_id_or_handle_table(id_or_handle))
61
+ .executeTakeFirst();
62
+ }
63
+ }
64
+
65
+
66
+ /**
67
+ * @param {SQL} driver
68
+ * @returns {db_col["remove"]}
69
+ */
70
+ const remove = (driver) => {
71
+ return async (id_or_handle) => {
72
+ const img = await driver.client
73
+ .selectFrom(table_name)
74
+ .selectAll()
75
+ .where(where_id_or_handle_table(id_or_handle))
76
+ .executeTakeFirst();
77
+
78
+ try {
79
+ await driver.client.transaction().execute(
80
+ async (trx) => {
81
+ // remove images -> media
82
+ await trx
83
+ .deleteFrom('entity_to_media')
84
+ .where('value', '=', img.url)
85
+ .execute();
86
+ // entities
87
+ await delete_search_of(trx, id_or_handle);
88
+ // delete me
89
+ await delete_me(trx, table_name, id_or_handle);
90
+ }
91
+ );
92
+
93
+ } catch(e) {
94
+ console.log(e);
95
+ return false;
96
+ }
97
+ return true;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * report media usages
103
+ * @param {SQL} driver
104
+ * @returns {db_col["report_document_media"]}
105
+ */
106
+ export const report_document_media = (driver) => {
107
+ /**
108
+ * @param {Transaction<import('../index.js').Database>} [transaction]
109
+ */
110
+ return async (item, transaction) => {
111
+ if(!(item?.media?.length))
112
+ return;
113
+
114
+ /**
115
+ *
116
+ * @param {Transaction<import('../index.js').Database>} trx
117
+ */
118
+ const doit = async (trx) => {
119
+ const dates = func.apply_dates({});
120
+
121
+ const ms = item.media.map(
122
+ m => (
123
+ {
124
+ handle: image_url_to_handle(m),
125
+ url: m,
126
+ name: image_url_to_name(m),
127
+ id: ID('img'),
128
+ created_at: dates.created_at,
129
+ updated_at: dates.updated_at,
130
+ }
131
+ )
132
+ );
133
+ const handles = ms.map(m => m.handle);
134
+
135
+ await trx.deleteFrom(table_name).where(
136
+ 'handle', 'in', handles
137
+ ).execute();
138
+ await trx.insertInto(table_name).values(
139
+ ms
140
+ ).execute();
141
+ // search stuff
142
+ // remove by reporter
143
+ await trx.deleteFrom('entity_to_search_terms').where(
144
+ eb => eb.and([
145
+ eb('reporter', '=', item.id),
146
+ eb('context', '=', table_name),
147
+ ]
148
+ )
149
+ ).execute();
150
+ const search = func.union(
151
+ item['title'], func.to_tokens(item['title'])
152
+ );
153
+ if(search.length) {
154
+ const A = ms.map(m => ({
155
+ entity_id: m.id,
156
+ entity_handle: m.handle,
157
+ context: table_name,
158
+ reporter: item.id
159
+ })
160
+ );
161
+
162
+ const B = search.reduce(
163
+ (p, c) => {
164
+ p.push(...A.map(a => ({...a, value: c})));
165
+ return p;
166
+ }, []
167
+ );
168
+
169
+ await trx.insertInto('entity_to_search_terms').values(
170
+ B
171
+ ).execute();
172
+ }
173
+
174
+ }
175
+
176
+ if(transaction) {
177
+ await doit(transaction);
178
+ } else {
179
+ try {
180
+ const t = await driver.client
181
+ .transaction()
182
+ .execute(doit);
183
+ } catch(e) {
184
+ console.log(e);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {SQL} driver
192
+ * @returns {db_col["list"]}
193
+ */
194
+ const list = (driver) => {
195
+ return async (query) => {
196
+
197
+ const items = await driver.client
198
+ .selectFrom(table_name)
199
+ .selectAll()
200
+ .where(
201
+ (eb) => {
202
+ return query_to_eb(eb, query, table_name);
203
+ }
204
+ )
205
+ .orderBy(query_to_sort(query))
206
+ .limit(query.limitToLast ?? query.limit ?? 10)
207
+ .execute();
208
+
209
+ if(query.limitToLast) items.reverse();
210
+
211
+ return sanitize_array(items);
212
+ }
213
+ }
214
+
215
+
216
+ /**
217
+ * @param {SQL} driver
218
+ * @return {db_col}}
219
+ * */
220
+ export const impl = (driver) => {
221
+
222
+ return {
223
+ get: get(driver),
224
+ upsert: upsert(driver),
225
+ remove: remove(driver),
226
+ list: list(driver),
227
+ report_document_media: report_document_media(driver),
228
+ count: count_regular(driver, table_name),
229
+ }
230
+ }
@@ -0,0 +1,149 @@
1
+ import { SQL } from '../driver.js'
2
+ import { count_regular, delete_me, delete_search_of,
3
+ insert_search_of, regular_upsert_me, where_id_or_handle_table,
4
+ with_search } from './con.shared.js'
5
+ import { sanitize_array } from './utils.funcs.js'
6
+ import { query_to_eb, query_to_sort } from './utils.query.js'
7
+
8
+ /**
9
+ * @typedef {import('@storecraft/core/v-database').db_notifications} db_col
10
+ */
11
+ export const table_name = 'notifications'
12
+
13
+ /**
14
+ * @param {SQL} driver
15
+ * @returns {db_col["upsert"]}
16
+ */
17
+ const upsert = (driver) => {
18
+ return async (item, search_terms=[]) => {
19
+ const c = driver.client;
20
+ try {
21
+ const t = await c.transaction().execute(
22
+ async (trx) => {
23
+ await insert_search_of(
24
+ trx, [...item.search, ...search_terms],
25
+ item.id, item.id, table_name
26
+ );
27
+ await regular_upsert_me(trx, table_name, {
28
+ created_at: item.created_at,
29
+ updated_at: item.updated_at,
30
+ message: item.message,
31
+ author: item.author,
32
+ id: item.id,
33
+ actions: JSON.stringify(item.actions)
34
+ });
35
+ }
36
+ );
37
+ } catch(e) {
38
+ console.log(e);
39
+ return false;
40
+ }
41
+ return true;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @param {SQL} driver
47
+ * @returns {db_col["upsertBulk"]}
48
+ */
49
+ const upsertBulk = (driver) => {
50
+ return async (items, search_terms) => {
51
+ const results = [];
52
+ // for (const it of items)
53
+ for(let ix = 0; ix < items.length; ix++)
54
+ results.push(await upsert(driver)(
55
+ items[ix], search_terms?.[ix])
56
+ );
57
+
58
+ return results.every(b => b);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @param {SQL} driver
64
+ * @returns {db_col["get"]}
65
+ */
66
+ const get = (driver) => {
67
+ return (id_or_handle, options) => {
68
+ return driver.client
69
+ .selectFrom(table_name)
70
+ .selectAll()
71
+ .select(eb => [
72
+ with_search(eb, eb.ref('notifications.id'), driver.dialectType),
73
+ ]
74
+ .filter(Boolean))
75
+ .where(where_id_or_handle_table(id_or_handle))
76
+ .executeTakeFirst();
77
+ }
78
+ }
79
+
80
+
81
+ /**
82
+ * @param {SQL} driver
83
+ * @returns {db_col["remove"]}
84
+ */
85
+ const remove = (driver) => {
86
+ return async (id_or_handle) => {
87
+ try {
88
+ await driver.client.transaction().execute(
89
+ async (trx) => {
90
+ // entities
91
+ await delete_search_of(trx, id_or_handle);
92
+ // delete me
93
+ await delete_me(trx, table_name, id_or_handle);
94
+ }
95
+ );
96
+
97
+ } catch(e) {
98
+ console.log(e);
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ }
104
+
105
+
106
+ /**
107
+ * @param {SQL} driver
108
+ * @returns {db_col["list"]}
109
+ */
110
+ const list = (driver) => {
111
+ return async (query) => {
112
+
113
+ const items = await driver.client
114
+ .selectFrom(table_name)
115
+ .selectAll()
116
+ .select(eb => [
117
+ with_search(eb, eb.ref('notifications.id'), driver.dialectType),
118
+ ].filter(Boolean))
119
+ .where(
120
+ (eb) => {
121
+ return query_to_eb(eb, query, table_name);
122
+ }
123
+ )
124
+ .orderBy(query_to_sort(query))
125
+ .limit(query.limitToLast ?? query.limit ?? 10)
126
+ .execute();
127
+
128
+ if(query.limitToLast) items.reverse();
129
+
130
+ return sanitize_array(items);
131
+ }
132
+ }
133
+
134
+
135
+ /**
136
+ * @param {SQL} driver
137
+ * @return {db_col}}
138
+ * */
139
+ export const impl = (driver) => {
140
+
141
+ return {
142
+ get: get(driver),
143
+ upsert: upsert(driver),
144
+ upsertBulk: upsertBulk(driver),
145
+ remove: remove(driver),
146
+ list: list(driver),
147
+ count: count_regular(driver, table_name),
148
+ }
149
+ }