@1auth/store-sql 0.0.0-rc.7 → 0.0.0-rc.8

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/index.js +158 -141
  4. package/package.json +2 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 will Farrell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <p>
6
6
  <a href="https://github.com/willfarrell/1auth/actions/workflows/test-unit.yml"><img src="https://github.com/willfarrell/1auth/actions/workflows/test-unit.yml/badge.svg" alt="GitHub Actions unit test status"></a>
7
7
  <a href="https://github.com/willfarrell/1auth/actions/workflows/test-dast.yml"><img src="https://github.com/willfarrell/1auth/actions/workflows/test-dast.yml/badge.svg" alt="GitHub Actions dast test status"></a>
8
- <a href="https://github.com/willfarrell/1auth/actions/workflows/test-perf.yml"><img src="https://github.com/willfarrell/1auth/actions/workflows/test-perf.yml/badge.svg" alt="GitHub Actions perf test status"></a>
8
+ <a href="https://github.com/willfarrell/1auth/actions/workflows/test-bench.yml"><img src="https://github.com/willfarrell/1auth/actions/workflows/test-bench.yml/badge.svg" alt="GitHub Actions perf test status"></a>
9
9
  <a href="https://github.com/willfarrell/1auth/actions/workflows/test-sast.yml"><img src="https://github.com/willfarrell/1auth/actions/workflows/test-sast.yml/badge.svg" alt="GitHub Actions SAST test status"></a>
10
10
  <a href="https://github.com/willfarrell/1auth/actions/workflows/test-lint.yml"><img src="https://github.com/willfarrell/1auth/actions/workflows/test-lint.yml/badge.svg" alt="GitHub Actions lint test status"></a>
11
11
  <br/>
package/index.js CHANGED
@@ -1,116 +1,129 @@
1
1
  // Copyright 2003 - 2026 will Farrell, and 1Auth contributors.
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- // Generic SQL store, usable on its own against any client exposing
5
- // query(sql, parameters) -> rows[]
6
- // The dialect-specific backends (@1auth/store-postgres, @1auth/store-sqlite,
7
- // @1auth/store-d1) each carry their own copy of this, customized for their
8
- // placeholder style and client shape, so none of them depend on this package.
4
+ export const createSqlStore = ({
5
+ id,
6
+ placeholder = "?",
7
+ query = (client, sql, parameters) => client.query(sql, parameters),
8
+ }) => {
9
+ const options = {
10
+ id,
11
+ log: undefined,
12
+ client: undefined,
13
+ // 10d chosen based on EFF DNT Policy
14
+ timeToLiveExpireOffset: 10 * 24 * 60 * 60,
15
+ timeToLiveKey: "remove",
16
+ placeholder,
17
+ };
9
18
 
10
- const options = {
11
- id: "sql",
12
- // Stryker disable next-line BooleanLiteral: overwritten by default() before observation
13
- log: false,
14
- // Stryker disable next-line ObjectLiteral: overwritten by default() before observation
15
- client: { query: undefined }, // async (sql, parameters) => rows[]
16
- // number of seconds after expire before removal
17
- // 10d chosen based on EFF DNT Policy
18
- timeToLiveExpireOffset: 10 * 24 * 60 * 60,
19
- timeToLiveKey: "remove",
20
- placeholder: "?",
21
- };
19
+ const configure = (opt = {}) => {
20
+ Object.assign(options, opt);
21
+ };
22
22
 
23
- export default (opt = {}) => {
24
- Object.assign(options, opt);
25
- };
23
+ const run = (sql, parameters) => query(options.client, sql, parameters);
26
24
 
27
- export const exists = async (table, filters) => {
28
- log(options, "exists", table, filters);
29
- const { select, where, parameters } = makeSqlParts(filters, {}, ["sub"]);
30
- const sql = `SELECT ${select} FROM ${table} ${where} LIMIT 1`;
31
- const res = await options.client.query(sql, parameters);
32
- return res?.[0]?.sub;
33
- };
25
+ const getPlaceholder = (idx) => getPlaceholderFor(options.placeholder, idx);
26
+ const makeSqlParts = (filters, values, fields, idxStart) =>
27
+ makeSqlPartsFor(options.placeholder, filters, values, fields, idxStart);
34
28
 
35
- export const count = async (table, filters = {}) => {
36
- log(options, "count", table, filters);
37
- const { where, parameters } = makeSqlParts(filters, {});
38
- const sql = `SELECT COUNT(*) AS count FROM ${table} ${where}`;
39
- const res = await options.client.query(sql, parameters);
40
- return Number(res[0].count);
41
- };
29
+ const exists = async (table, filters) => {
30
+ log(options, "exists", table, filters);
31
+ const { select, where, parameters } = makeSqlParts(filters, {}, ["sub"]);
32
+ const sql = `SELECT ${select} FROM ${table} ${where} LIMIT 1`;
33
+ const res = await run(sql, parameters);
34
+ return res?.[0]?.sub;
35
+ };
42
36
 
43
- export const select = async (table, filters = {}, fields = []) => {
44
- log(options, "select", table, filters, fields);
45
- const { select, where, parameters } = makeSqlParts(filters, {}, fields);
46
- const sql = `SELECT ${select} FROM ${table} ${where} LIMIT 1`;
47
- const res = await options.client.query(sql, parameters);
48
- const row = res?.[0];
49
- // Workaround because an expire filter doesn't exist yet
50
- parseValues(row);
51
- return row;
52
- };
53
-
54
- export const selectList = async (table, filters = {}, fields = []) => {
55
- log(options, "selectList", table, filters, fields);
56
- const { select, where, parameters } = makeSqlParts(filters, {}, fields);
57
- const sql = `SELECT ${select} FROM ${table} ${where}`;
58
- const rows = await options.client.query(sql, parameters);
59
- // Workaround because an expire filter doesn't exist yet
60
- return rows.map((row) => {
37
+ const select = async (table, filters = {}, fields = []) => {
38
+ log(options, "select", table, filters, fields);
39
+ const { select, where, parameters } = makeSqlParts(filters, {}, fields);
40
+ const sql = `SELECT ${select} FROM ${table} ${where} LIMIT 1`;
41
+ const res = await run(sql, parameters);
42
+ const row = res?.[0];
61
43
  parseValues(row);
62
44
  return row;
63
- });
64
- };
45
+ };
65
46
 
66
- export const insert = async (table, inputValues = {}) => {
67
- const values = makeValues(inputValues, options);
68
- log(options, "insert", table, values);
69
- const { insert, parameters } = makeSqlParts({}, values);
70
- const sql = `INSERT INTO ${table} ${insert} RETURNING id`;
71
- const res = await options.client.query(sql, parameters);
72
- return res[0].id;
73
- };
47
+ const selectList = async (table, filters = {}, fields = []) => {
48
+ log(options, "selectList", table, filters, fields);
49
+ const { select, where, parameters } = makeSqlParts(filters, {}, fields);
50
+ const sql = `SELECT ${select} FROM ${table} ${where}`;
51
+ const rows = await run(sql, parameters);
52
+ return rows.map((row) => {
53
+ parseValues(row);
54
+ return row;
55
+ });
56
+ };
74
57
 
75
- export const insertList = async (table, rows = []) => {
76
- log(options, "insertList", table, rows);
77
- if (!rows.length) return [];
78
- const { insert, parameters } = makeInsertList(
79
- options.placeholder,
80
- rows.map((row) => makeValues(row, options)),
81
- );
82
- const sql = `INSERT INTO ${table} ${insert} RETURNING id`;
83
- return await options.client.query(sql, parameters);
84
- };
58
+ const insert = async (table, inputValues = {}) => {
59
+ const values = makeValues(inputValues, options);
60
+ log(options, "insert", table, values);
61
+ const { insert, parameters } = makeSqlParts({}, values);
62
+ const sql = `INSERT INTO ${table} ${insert} RETURNING id`;
63
+ const res = await run(sql, parameters);
64
+ return res[0].id;
65
+ };
85
66
 
86
- export const update = async (table, filters = {}, inputValues = {}) => {
87
- const values = makeValues(inputValues, options);
88
- log(options, "update", table, filters, values);
89
- const { update, where, parameters } = makeSqlParts(filters, values);
90
- const sql = `UPDATE ${table} SET ${update} ${where}`;
91
- await options.client.query(sql, parameters);
92
- };
67
+ const insertList = async (table, rows = []) => {
68
+ log(options, "insertList", table, rows);
69
+ if (!rows.length) return [];
70
+ const { insert, parameters } = makeInsertList(
71
+ options.placeholder,
72
+ rows.map((row) => makeValues(row, options)),
73
+ );
74
+ const sql = `INSERT INTO ${table} ${insert} RETURNING id`;
75
+ return await run(sql, parameters);
76
+ };
93
77
 
94
- export const updateList = async (table, filtersList = [], values = {}) => {
95
- log(options, "updateList", table, filtersList, values);
96
- return await Promise.allSettled(
97
- filtersList.map((filters) => update(table, filters, values)),
98
- );
99
- };
78
+ const update = async (table, filters = {}, inputValues = {}) => {
79
+ const values = makeValues(inputValues, options);
80
+ log(options, "update", table, filters, values);
81
+ const { update, where, parameters } = makeSqlParts(filters, values);
82
+ assertWhere(table, where);
83
+ const sql = `UPDATE ${table} SET ${update} ${where}`;
84
+ await run(sql, parameters);
85
+ };
86
+
87
+ const remove = async (table, filters = {}) => {
88
+ log(options, "remove", table, filters);
89
+ const { where, parameters } = makeSqlParts(filters);
90
+ assertWhere(table, where);
91
+ const sql = `DELETE FROM ${table} ${where} RETURNING id`;
92
+ const res = await run(sql, parameters);
93
+ return !!res?.[0]?.id;
94
+ };
100
95
 
101
- export const remove = async (table, filters = {}) => {
102
- log(options, "remove", table, filters);
103
- const { where, parameters } = makeSqlParts(filters);
104
- const sql = `DELETE FROM ${table} ${where} RETURNING id`;
105
- const res = await options.client.query(sql, parameters);
106
- return !!res?.[0]?.id;
96
+ return {
97
+ default: configure,
98
+ exists,
99
+ select,
100
+ selectList,
101
+ insert,
102
+ insertList,
103
+ update,
104
+ remove,
105
+ removeList: remove,
106
+ getPlaceholder,
107
+ makeSqlParts,
108
+ };
107
109
  };
108
110
 
109
- export const removeList = remove;
111
+ const store = createSqlStore({ id: "sql" });
112
+ export default store.default;
113
+ export const {
114
+ exists,
115
+ select,
116
+ selectList,
117
+ insert,
118
+ insertList,
119
+ update,
120
+ remove,
121
+ removeList,
122
+ getPlaceholder,
123
+ makeSqlParts,
124
+ } = store;
110
125
 
111
126
  // --- builders -------------------------------------------------------------
112
- // The `*For` variants take the placeholder explicitly; the bound pair at the
113
- // bottom reads this store's options.
114
127
 
115
128
  export const log = (options, method, ...args) => {
116
129
  if (options.log) {
@@ -118,6 +131,22 @@ export const log = (options, method, ...args) => {
118
131
  }
119
132
  };
120
133
 
134
+ const identifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
135
+ const quote = (key) => {
136
+ if (!identifierPattern.test(key)) {
137
+ throw new Error("@1auth/store-sql invalid identifier", { cause: { key } });
138
+ }
139
+ return `"${key}"`;
140
+ };
141
+
142
+ const assertWhere = (table, where) => {
143
+ if (!where) {
144
+ throw new Error("@1auth/store-sql refusing to write without a filter", {
145
+ cause: { table },
146
+ });
147
+ }
148
+ };
149
+
121
150
  export const getPlaceholderFor = (placeholder, idx) => {
122
151
  return placeholder === "$" ? `$${idx}` : placeholder;
123
152
  };
@@ -133,16 +162,16 @@ export const makeSqlPartsFor = (
133
162
  let parameters = [];
134
163
  const keys = Object.keys(values);
135
164
 
136
- const select = fields.length ? `"${fields.join('", "')}"` : "*";
165
+ const select = fields.length ? fields.map(quote).join(", ") : "*";
137
166
 
138
167
  const insertParts = [];
139
168
  const updateParts = [];
140
169
  for (const key of keys) {
141
170
  insertParts.push(getPlaceholderFor(placeholder, idx));
142
- updateParts.push(`"${key}" = ${getPlaceholderFor(placeholder, idx)}`);
171
+ updateParts.push(`${quote(key)} = ${getPlaceholderFor(placeholder, idx)}`);
143
172
  idx++;
144
173
  }
145
- const insert = `("${keys.join('", "')}") VALUES (${insertParts.join(",")})`;
174
+ const insert = `(${keys.map(quote).join(", ")}) VALUES (${insertParts.join(",")})`;
146
175
  const update = updateParts.join(", ");
147
176
  parameters = parameters.concat(Object.values(values));
148
177
 
@@ -150,50 +179,48 @@ export const makeSqlPartsFor = (
150
179
  .map((key) => {
151
180
  const value = filters[key];
152
181
  if (typeof value === "undefined") {
153
- return null;
182
+ throw new Error("@1auth/store-sql filter value is undefined", {
183
+ cause: { key },
184
+ });
154
185
  }
155
186
  if (Array.isArray(value)) {
156
- let sql = value
187
+ if (!value.length) {
188
+ throw new Error("@1auth/store-sql filter value is an empty array", {
189
+ cause: { key },
190
+ });
191
+ }
192
+ const sql = value
157
193
  .map(() => getPlaceholderFor(placeholder, idx++))
158
194
  .join(",");
159
- sql &&= `"${key}" IN (${sql})`;
160
195
  parameters = parameters.concat(value);
161
- return sql;
196
+ return `${quote(key)} IN (${sql})`;
162
197
  }
163
- const sql = `"${key}" = ${getPlaceholderFor(placeholder, idx++)}`;
198
+ const sql = `${quote(key)} = ${getPlaceholderFor(placeholder, idx++)}`;
164
199
  parameters.push(value);
165
200
  return sql;
166
201
  })
167
- .filter((v) => v)
168
202
  .join(" AND ");
169
203
  where &&= `WHERE ${where}`;
170
204
 
171
205
  return { select, insert, update, where, parameters };
172
206
  };
173
207
 
174
- // Single multi-row `("a", "b") VALUES (?,?),(?,?)` for drivers that batch in one statement
175
208
  export const makeInsertList = (placeholder, valuesList) => {
176
- const insertValues = [];
177
- let insertParameters = [];
178
- for (let i = 0, l = valuesList.length; i < l; i++) {
179
- const values = valuesList[i];
180
- const { insert, parameters } = makeSqlPartsFor(
181
- placeholder,
182
- {},
183
- values,
184
- undefined, // no fields: `select` is unused here
185
- i * Object.keys(values).length + 1,
186
- );
187
- if (i) {
188
- insertValues.push(insert.split("VALUES")[1]); // (?)
189
- } else {
190
- insertValues.push(insert); // (name) VALUES (?)
191
- }
192
-
193
- insertParameters = insertParameters.concat(parameters);
194
- }
195
-
196
- return { insert: insertValues.join(","), parameters: insertParameters };
209
+ if (!valuesList.length) return { insert: "", parameters: [] };
210
+ const columns = [...new Set(valuesList.flatMap(Object.keys))];
211
+ const parameters = [];
212
+ let idx = 1;
213
+ const groups = valuesList.map((values) => {
214
+ const group = columns.map((column) => {
215
+ parameters.push(Object.hasOwn(values, column) ? values[column] : null);
216
+ return getPlaceholderFor(placeholder, idx++);
217
+ });
218
+ return `(${group.join(",")})`;
219
+ });
220
+ return {
221
+ insert: `(${columns.map(quote).join(", ")}) VALUES ${groups.join(", ")}`,
222
+ parameters,
223
+ };
197
224
  };
198
225
 
199
226
  export const withTimeToLive = (values, options) => {
@@ -228,16 +255,12 @@ export const normalizeValues = (values) => {
228
255
  values.expire &&= new Date(values.expire * 1000).toISOString();
229
256
  values.remove &&= new Date(values.remove * 1000).toISOString();
230
257
  for (const [key, v] of Object.entries(values)) {
231
- // Stryker disable next-line ConditionalExpression,StringLiteral: dropping the
232
- // string guard is an equivalent mutant -- strings then fall through to
233
- // String(v), which returns them unchanged.
234
- if (v !== null && typeof v !== "string" && typeof v !== "number") {
235
- values[key] =
236
- v === undefined
237
- ? null
238
- : typeof v === "object"
239
- ? JSON.stringify(v)
240
- : String(v);
258
+ if (v === undefined) {
259
+ values[key] = null;
260
+ } else if (v !== null && typeof v === "object") {
261
+ values[key] = JSON.stringify(v);
262
+ } else if (v !== null && typeof v !== "number") {
263
+ values[key] = String(v);
241
264
  }
242
265
  }
243
266
  };
@@ -254,9 +277,3 @@ export const parseValues = (values) => {
254
277
  values.expire &&= Date.parse(values.expire) / 1000;
255
278
  values.remove &&= Date.parse(values.remove) / 1000;
256
279
  };
257
-
258
- // export for testing
259
- export const getPlaceholder = (idx) =>
260
- getPlaceholderFor(options.placeholder, idx);
261
- export const makeSqlParts = (filters, values, fields, idxStart) =>
262
- makeSqlPartsFor(options.placeholder, filters, values, fields, idxStart);
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@1auth/store-sql",
3
- "version": "0.0.0-rc.7",
3
+ "version": "0.0.0-rc.8",
4
4
  "description": "Generic SQL storage implementation for any client exposing a query method",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=24"
7
+ "node": ">=26"
8
8
  },
9
- "engineStrict": true,
10
9
  "publishConfig": {
11
10
  "access": "public"
12
11
  },