@getstrata/core 1.0.5 → 1.0.7

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.
@@ -1,4 +1,28 @@
1
1
  // @bun
2
+ // ../../src/core/database/pluck.ts
3
+ function uniqueColumnSelect(table, columns) {
4
+ const seen = new Set;
5
+ const select = [];
6
+ for (const column of columns) {
7
+ if (seen.has(column)) {
8
+ continue;
9
+ }
10
+ seen.add(column);
11
+ select.push({ kind: "column", table, column, as: column });
12
+ }
13
+ return select;
14
+ }
15
+ function projectPluck(rows, column, keyBy) {
16
+ if (keyBy === undefined) {
17
+ return rows.map((row) => row[column]);
18
+ }
19
+ const keyed = new Map;
20
+ for (const row of rows) {
21
+ keyed.set(row[keyBy], row[column]);
22
+ }
23
+ return keyed;
24
+ }
25
+
2
26
  // ../../src/core/database/query.ts
3
27
  import { currentSqlDialect } from "@getstrata/core/database/dialect";
4
28
  function quoteIdentifier(identifier) {
@@ -37,15 +61,32 @@ function pushParam(values, value) {
37
61
  values.push(value);
38
62
  return currentSqlDialect().placeholder(values.length);
39
63
  }
40
- function buildInClause(column, values, params) {
64
+ var SUPPORTED_OPERATORS = new Set([
65
+ "eq",
66
+ "ne",
67
+ "in",
68
+ "notIn",
69
+ "gt",
70
+ "gte",
71
+ "lt",
72
+ "lte",
73
+ "isNull",
74
+ "ilike",
75
+ "tsMatch"
76
+ ]);
77
+ function buildInClause(column, values, params, negated = false) {
41
78
  if (values.length === 0) {
42
- return "1 = 0";
79
+ return negated ? "1 = 1" : "1 = 0";
43
80
  }
44
81
  const placeholders = values.map((value) => pushParam(params, value)).join(", ");
45
- return `${column} IN (${placeholders})`;
82
+ return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
46
83
  }
47
84
  function buildOperatorClauses(column, operator, params) {
48
85
  const clauses = [];
86
+ const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
87
+ if (unsupported.length > 0) {
88
+ throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
89
+ }
49
90
  if (operator.isNull === true) {
50
91
  clauses.push(`${column} IS NULL`);
51
92
  }
@@ -59,9 +100,19 @@ function buildOperatorClauses(column, operator, params) {
59
100
  clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
60
101
  }
61
102
  }
103
+ if (operator.ne !== undefined) {
104
+ if (operator.ne === null) {
105
+ clauses.push(`${column} IS NOT NULL`);
106
+ } else {
107
+ clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
108
+ }
109
+ }
62
110
  if (operator.in !== undefined) {
63
111
  clauses.push(buildInClause(column, operator.in, params));
64
112
  }
113
+ if (operator.notIn !== undefined) {
114
+ clauses.push(buildInClause(column, operator.notIn, params, true));
115
+ }
65
116
  if (operator.gt !== undefined) {
66
117
  clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
67
118
  }
@@ -281,6 +332,10 @@ function buildSelectList(table, select, params = []) {
281
332
  if (item.kind === "literalText") {
282
333
  return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
283
334
  }
335
+ if (item.kind === "subqueryCount") {
336
+ const body = remapExistsSql(item.sql, item.params, params);
337
+ return `(${body}) AS ${quoteIdentifier(item.as)}`;
338
+ }
284
339
  const column = qualifyColumn(item.table, item.column);
285
340
  const placeholder = pushParam(params, item.query);
286
341
  return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
@@ -375,6 +430,52 @@ function buildInsertQuery(table, values) {
375
430
  params
376
431
  };
377
432
  }
433
+ function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
434
+ const entries = getDefinedColumnEntries(table, values);
435
+ if (entries.length === 0) {
436
+ throw new Error(`Cannot upsert into ${table.name} without any column values.`);
437
+ }
438
+ if (conflictColumns.length === 0) {
439
+ throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
440
+ }
441
+ const insertable = new Set(entries.map(([column]) => column));
442
+ const conflict = new Set(conflictColumns);
443
+ const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
444
+ const params = [];
445
+ const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
446
+ const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
447
+ const returningColumns = buildReturningColumns(table);
448
+ const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
449
+ return {
450
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
451
+ params
452
+ };
453
+ }
454
+ function buildIncrementQuery(table, id, column, amount, extra = {}) {
455
+ if (!Number.isFinite(amount)) {
456
+ throw new Error("Increment amount must be a finite number.");
457
+ }
458
+ if (!table.columns.includes(column)) {
459
+ throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
460
+ }
461
+ const params = [];
462
+ const target = quoteIdentifier(column);
463
+ const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
464
+ for (const [name, value] of getDefinedColumnEntries(table, extra, {
465
+ exclude: [table.primaryKey, column]
466
+ })) {
467
+ assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
468
+ }
469
+ const primaryKeyPlaceholder = pushParam(params, id);
470
+ const returningColumns = buildReturningColumns(table);
471
+ const scopeClauses = [];
472
+ appendSoftDeleteScope(table, {}, scopeClauses);
473
+ const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
474
+ return {
475
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
476
+ params
477
+ };
478
+ }
378
479
  function buildUpdateQuery(table, id, changes) {
379
480
  const entries = getDefinedColumnEntries(table, changes, {
380
481
  exclude: [table.primaryKey]
@@ -755,6 +856,22 @@ class RepositoryQuery {
755
856
  whereNotNull(column) {
756
857
  return this.where({ [column]: { isNull: false } });
757
858
  }
859
+ whereNotIn(column, values) {
860
+ return this.where({ [column]: { notIn: values } });
861
+ }
862
+ withSubqueryCount(alias, sql, params = []) {
863
+ const table = this.repository.getTable();
864
+ const existing = this.queryOptions.select ?? table.columns.map((column) => ({
865
+ kind: "column",
866
+ table: table.name,
867
+ column
868
+ }));
869
+ this.queryOptions = {
870
+ ...this.queryOptions,
871
+ select: [...existing, { kind: "subqueryCount", sql, params, as: alias }]
872
+ };
873
+ return this;
874
+ }
758
875
  whereIn(column, values) {
759
876
  return this.where({ [column]: values });
760
877
  }
@@ -875,6 +992,28 @@ class RepositoryQuery {
875
992
  async count() {
876
993
  return await this.repository.count(this.buildOptions());
877
994
  }
995
+ async pluck(column, keyBy) {
996
+ const rows = await this.repository.findAll({
997
+ ...this.buildOptions(),
998
+ select: uniqueColumnSelect(this.repository.getTable().name, keyBy === undefined || keyBy === column ? [column] : [column, keyBy])
999
+ });
1000
+ if (keyBy === undefined) {
1001
+ return projectPluck(rows, column);
1002
+ }
1003
+ return projectPluck(rows, column, keyBy);
1004
+ }
1005
+ async value(column) {
1006
+ const rows = await this.repository.findAll({
1007
+ ...this.buildOptions(),
1008
+ select: uniqueColumnSelect(this.repository.getTable().name, [column]),
1009
+ limit: 1
1010
+ });
1011
+ const row = rows[0];
1012
+ if (row === undefined) {
1013
+ return null;
1014
+ }
1015
+ return row[column];
1016
+ }
878
1017
  async attachToRows(rows) {
879
1018
  return await this.attach(rows);
880
1019
  }
@@ -980,5 +1119,7 @@ class RepositoryQuery {
980
1119
  }
981
1120
  }
982
1121
  export {
983
- RepositoryQuery
1122
+ RepositoryQuery,
1123
+ projectPluck,
1124
+ uniqueColumnSelect
984
1125
  };
@@ -304,15 +304,32 @@ function pushParam(values, value) {
304
304
  values.push(value);
305
305
  return currentSqlDialect().placeholder(values.length);
306
306
  }
307
- function buildInClause(column, values, params) {
307
+ var SUPPORTED_OPERATORS = new Set([
308
+ "eq",
309
+ "ne",
310
+ "in",
311
+ "notIn",
312
+ "gt",
313
+ "gte",
314
+ "lt",
315
+ "lte",
316
+ "isNull",
317
+ "ilike",
318
+ "tsMatch"
319
+ ]);
320
+ function buildInClause(column, values, params, negated = false) {
308
321
  if (values.length === 0) {
309
- return "1 = 0";
322
+ return negated ? "1 = 1" : "1 = 0";
310
323
  }
311
324
  const placeholders = values.map((value) => pushParam(params, value)).join(", ");
312
- return `${column} IN (${placeholders})`;
325
+ return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
313
326
  }
314
327
  function buildOperatorClauses(column, operator, params) {
315
328
  const clauses = [];
329
+ const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
330
+ if (unsupported.length > 0) {
331
+ throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
332
+ }
316
333
  if (operator.isNull === true) {
317
334
  clauses.push(`${column} IS NULL`);
318
335
  }
@@ -326,9 +343,19 @@ function buildOperatorClauses(column, operator, params) {
326
343
  clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
327
344
  }
328
345
  }
346
+ if (operator.ne !== undefined) {
347
+ if (operator.ne === null) {
348
+ clauses.push(`${column} IS NOT NULL`);
349
+ } else {
350
+ clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
351
+ }
352
+ }
329
353
  if (operator.in !== undefined) {
330
354
  clauses.push(buildInClause(column, operator.in, params));
331
355
  }
356
+ if (operator.notIn !== undefined) {
357
+ clauses.push(buildInClause(column, operator.notIn, params, true));
358
+ }
332
359
  if (operator.gt !== undefined) {
333
360
  clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
334
361
  }
@@ -548,6 +575,10 @@ function buildSelectList(table, select, params = []) {
548
575
  if (item.kind === "literalText") {
549
576
  return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
550
577
  }
578
+ if (item.kind === "subqueryCount") {
579
+ const body = remapExistsSql(item.sql, item.params, params);
580
+ return `(${body}) AS ${quoteIdentifier(item.as)}`;
581
+ }
551
582
  const column = qualifyColumn(item.table, item.column);
552
583
  const placeholder = pushParam(params, item.query);
553
584
  return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
@@ -642,6 +673,52 @@ function buildInsertQuery(table, values) {
642
673
  params
643
674
  };
644
675
  }
676
+ function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
677
+ const entries = getDefinedColumnEntries(table, values);
678
+ if (entries.length === 0) {
679
+ throw new Error(`Cannot upsert into ${table.name} without any column values.`);
680
+ }
681
+ if (conflictColumns.length === 0) {
682
+ throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
683
+ }
684
+ const insertable = new Set(entries.map(([column]) => column));
685
+ const conflict = new Set(conflictColumns);
686
+ const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
687
+ const params = [];
688
+ const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
689
+ const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
690
+ const returningColumns = buildReturningColumns(table);
691
+ const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
692
+ return {
693
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
694
+ params
695
+ };
696
+ }
697
+ function buildIncrementQuery(table, id, column, amount, extra = {}) {
698
+ if (!Number.isFinite(amount)) {
699
+ throw new Error("Increment amount must be a finite number.");
700
+ }
701
+ if (!table.columns.includes(column)) {
702
+ throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
703
+ }
704
+ const params = [];
705
+ const target = quoteIdentifier(column);
706
+ const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
707
+ for (const [name, value] of getDefinedColumnEntries(table, extra, {
708
+ exclude: [table.primaryKey, column]
709
+ })) {
710
+ assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
711
+ }
712
+ const primaryKeyPlaceholder = pushParam(params, id);
713
+ const returningColumns = buildReturningColumns(table);
714
+ const scopeClauses = [];
715
+ appendSoftDeleteScope(table, {}, scopeClauses);
716
+ const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
717
+ return {
718
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
719
+ params
720
+ };
721
+ }
645
722
  function buildUpdateQuery(table, id, changes) {
646
723
  const entries = getDefinedColumnEntries(table, changes, {
647
724
  exclude: [table.primaryKey]
@@ -2,6 +2,26 @@
2
2
  // ../../src/core/http/parseMultipartUpload.ts
3
3
  import { BadRequestError, PayloadTooLargeError } from "@getstrata/core/errors/http";
4
4
 
5
+ // ../../src/core/runtime/appEnv.ts
6
+ var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
7
+ function normalizeEnvValue(value) {
8
+ return (value ?? "").trim().toLowerCase();
9
+ }
10
+ function isProductionEnv(env = process.env) {
11
+ const appEnv = normalizeEnvValue(env.APP_ENV);
12
+ const nodeEnv = normalizeEnvValue(env.NODE_ENV);
13
+ if (appEnv === "production" || nodeEnv === "production") {
14
+ return true;
15
+ }
16
+ if (appEnv === "") {
17
+ return false;
18
+ }
19
+ return !NON_PRODUCTION_APP_ENVS.has(appEnv);
20
+ }
21
+ function envFlagEnabled(value) {
22
+ return value === "true";
23
+ }
24
+
5
25
  // ../../src/core/http/uploads.ts
6
26
  var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
7
27
  var ALLOWED_UPLOAD_MIME_TYPES = new Set([
@@ -33,7 +53,7 @@ function normalizeMimeType(mimeType) {
33
53
  function isAllowedMimeType(mimeType) {
34
54
  const normalized = normalizeMimeType(mimeType);
35
55
  if (!normalized || normalized === "application/octet-stream") {
36
- return true;
56
+ return envFlagEnabled(process.env.UPLOAD_ALLOW_UNKNOWN_MIME);
37
57
  }
38
58
  return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
39
59
  }
@@ -52,7 +52,7 @@ export { createDatabaseQueryProxy } from "../core/database/queryProxy.ts";
52
52
  export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation, } from "../core/database/relationships.ts";
53
53
  export { belongsTo, belongsToMany, hasMany, hasManyThrough, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasManyThroughRelation, indexHasOneRelation, indexMorphManyRelation, indexMorphOneRelation, indexMorphToRelation, morphMany, morphOne, morphTo, } from "../core/database/relationships.ts";
54
54
  export { repositoryConnection, resolveRepositoryConnection, } from "../core/database/repositoryConnection.ts";
55
- export { RepositoryQuery } from "../core/database/repositoryQuery.ts";
55
+ export { projectPluck, RepositoryQuery, uniqueColumnSelect, } from "../core/database/repositoryQuery.ts";
56
56
  export type { BlueprintAction, BlueprintCallback, ColumnKind, DatabaseDriver, ForeignKeyOptions, Grammar, IndexDefinition, IndexKind, ResolveDatabaseDriverOptions, SchemaBuilder, } from "../core/database/schema/index.ts";
57
57
  export { Blueprint, ColumnDefinition, compileBlueprint, createSchemaBuilder, ForeignIdColumnDefinition, grammarForDriver, inferReferencedTable, MySqlGrammar, PostgresGrammar, resolveDatabaseDriver, Schema, SqliteGrammar, UnsupportedSchemaFeatureError, } from "../core/database/schema/index.ts";
58
58
  export { loadSeedersFromDirectory, runSeedersFromDirectory, } from "../core/database/seeders/runner.ts";