@rebasepro/server-core 0.3.0 → 0.5.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 (50) hide show
  1. package/README.md +62 -25
  2. package/dist/common/src/collections/default-collections.d.ts +5 -8
  3. package/dist/common/src/data/query_builder.d.ts +6 -2
  4. package/dist/common/src/util/permissions.d.ts +14 -6
  5. package/dist/index.es.js +393 -315
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +393 -315
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/server-core/src/api/errors.d.ts +15 -0
  10. package/dist/server-core/src/api/rest/query-parser.d.ts +2 -0
  11. package/dist/server-core/src/api/types.d.ts +2 -1
  12. package/dist/server-core/src/auth/jwt.d.ts +10 -0
  13. package/dist/server-core/src/email/types.d.ts +1 -0
  14. package/dist/server-core/src/init.d.ts +31 -1
  15. package/dist/types/src/controllers/auth.d.ts +2 -2
  16. package/dist/types/src/controllers/client.d.ts +25 -40
  17. package/dist/types/src/controllers/data.d.ts +21 -3
  18. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  19. package/dist/types/src/controllers/email.d.ts +2 -0
  20. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  21. package/dist/types/src/types/backend.d.ts +38 -3
  22. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  23. package/dist/types/src/types/collections.d.ts +30 -6
  24. package/dist/types/src/types/entity_views.d.ts +19 -28
  25. package/dist/types/src/types/properties.d.ts +9 -15
  26. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  27. package/dist/types/src/users/index.d.ts +0 -1
  28. package/dist/types/src/users/user.d.ts +0 -1
  29. package/package.json +5 -5
  30. package/src/api/errors.ts +20 -1
  31. package/src/api/openapi-generator.ts +1 -1
  32. package/src/api/rest/api-generator.ts +82 -24
  33. package/src/api/rest/query-parser.ts +184 -63
  34. package/src/api/server.ts +1 -1
  35. package/src/api/types.ts +2 -1
  36. package/src/auth/admin-routes.ts +2 -91
  37. package/src/auth/builtin-auth-adapter.ts +9 -70
  38. package/src/auth/custom-auth-adapter.ts +1 -1
  39. package/src/auth/jwt.ts +10 -0
  40. package/src/auth/routes.ts +5 -9
  41. package/src/email/smtp-email-service.ts +31 -0
  42. package/src/email/types.ts +1 -0
  43. package/src/init.ts +135 -31
  44. package/src/storage/image-transform.ts +2 -1
  45. package/test/admin-routes.test.ts +0 -169
  46. package/test/backend-hooks-admin.test.ts +0 -25
  47. package/test/custom-auth-adapter.test.ts +2 -10
  48. package/test/smtp-email-service.test.ts +169 -0
  49. package/dist/types/src/users/roles.d.ts +0 -14
  50. package/test.ts +0 -6
package/dist/index.es.js CHANGED
@@ -146,6 +146,9 @@ class Vector {
146
146
  this.value = value;
147
147
  }
148
148
  }
149
+ function isPostgresCollection(collection) {
150
+ return !collection.driver || collection.driver === "postgres";
151
+ }
149
152
  function isSQLAdmin(admin) {
150
153
  return !!admin && typeof admin.executeSql === "function";
151
154
  }
@@ -2547,30 +2550,6 @@ class CollectionRegistry {
2547
2550
  };
2548
2551
  }
2549
2552
  }
2550
- function mapOperator$1(op) {
2551
- switch (op) {
2552
- case "==":
2553
- return "eq";
2554
- case "!=":
2555
- return "neq";
2556
- case ">":
2557
- return "gt";
2558
- case ">=":
2559
- return "gte";
2560
- case "<":
2561
- return "lt";
2562
- case "<=":
2563
- return "lte";
2564
- case "array-contains":
2565
- return "cs";
2566
- case "array-contains-any":
2567
- return "csa";
2568
- case "not-in":
2569
- return "nin";
2570
- default:
2571
- return op;
2572
- }
2573
- }
2574
2553
  class QueryBuilder {
2575
2554
  constructor(collection) {
2576
2555
  this.collection = collection;
@@ -2578,23 +2557,30 @@ class QueryBuilder {
2578
2557
  params = {
2579
2558
  where: {}
2580
2559
  };
2581
- /**
2582
- * Add a filter condition to your query.
2583
- * @example
2584
- * client.collection('users').where('age', '>=', 18).find()
2585
- */
2586
- where(column, operator, value) {
2560
+ where(columnOrCondition, operator, value) {
2561
+ if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
2562
+ this.params.logical = columnOrCondition;
2563
+ return this;
2564
+ }
2587
2565
  if (!this.params.where) {
2588
2566
  this.params.where = {};
2589
2567
  }
2590
- const mappedOp = mapOperator$1(operator);
2591
- let formattedValue = value;
2592
- if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
2593
- formattedValue = `(${value.join(",")})`;
2594
- } else if (value === null) {
2595
- formattedValue = "null";
2568
+ const column = columnOrCondition;
2569
+ const condition = [operator, value];
2570
+ const existing = this.params.where[column];
2571
+ if (existing === void 0) {
2572
+ this.params.where[column] = condition;
2573
+ } else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) {
2574
+ this.params.where[column].push(condition);
2575
+ } else {
2576
+ let firstCondition;
2577
+ if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") {
2578
+ firstCondition = existing;
2579
+ } else {
2580
+ firstCondition = ["==", existing];
2581
+ }
2582
+ this.params.where[column] = [firstCondition, condition];
2596
2583
  }
2597
- this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
2598
2584
  return this;
2599
2585
  }
2600
2586
  /**
@@ -2804,6 +2790,9 @@ class ApiError extends Error {
2804
2790
  return new ApiError(503, code2, message);
2805
2791
  }
2806
2792
  }
2793
+ function isRebaseApiError(error2) {
2794
+ return error2 instanceof Error;
2795
+ }
2807
2796
  const errorHandler = (err, c) => {
2808
2797
  const error2 = err;
2809
2798
  if (error2 instanceof ApiError || error2.name === "ApiError") {
@@ -2917,68 +2906,184 @@ function mapOperator(op) {
2917
2906
  return null;
2918
2907
  }
2919
2908
  }
2909
+ function getLastValue(val) {
2910
+ if (Array.isArray(val)) {
2911
+ return val[val.length - 1];
2912
+ }
2913
+ return val;
2914
+ }
2915
+ function parseLogicalString(str) {
2916
+ str = str.trim();
2917
+ if (str.startsWith("or(") && str.endsWith(")")) {
2918
+ const inner = str.slice(3, -1);
2919
+ return {
2920
+ type: "or",
2921
+ conditions: parseLogicalList(inner)
2922
+ };
2923
+ }
2924
+ if (str.startsWith("and(") && str.endsWith(")")) {
2925
+ const inner = str.slice(4, -1);
2926
+ return {
2927
+ type: "and",
2928
+ conditions: parseLogicalList(inner)
2929
+ };
2930
+ }
2931
+ const firstDot = str.indexOf(".");
2932
+ if (firstDot === -1) {
2933
+ return {
2934
+ column: str,
2935
+ operator: "==",
2936
+ value: true
2937
+ };
2938
+ }
2939
+ const field = str.substring(0, firstDot);
2940
+ const rest = str.substring(firstDot + 1);
2941
+ const secondDot = rest.indexOf(".");
2942
+ let op = "eq";
2943
+ let valStr = rest;
2944
+ if (secondDot !== -1) {
2945
+ op = rest.substring(0, secondDot);
2946
+ valStr = rest.substring(secondDot + 1);
2947
+ } else {
2948
+ op = "eq";
2949
+ valStr = rest;
2950
+ }
2951
+ const rebaseOp = mapOperator(op) || "==";
2952
+ let parsedVal = valStr;
2953
+ if (valStr === "true") parsedVal = true;
2954
+ else if (valStr === "false") parsedVal = false;
2955
+ else if (valStr === "null") parsedVal = null;
2956
+ else if (!isNaN(Number(valStr)) && valStr.trim() !== "") parsedVal = Number(valStr);
2957
+ else if (valStr.startsWith("(")) {
2958
+ const arrayContent = valStr.endsWith(")") ? valStr.slice(1, -1) : valStr.slice(1);
2959
+ parsedVal = arrayContent.split(",").map((v) => {
2960
+ const trimmed = v.trim();
2961
+ if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
2962
+ if (trimmed === "true") return true;
2963
+ if (trimmed === "false") return false;
2964
+ if (trimmed === "null") return null;
2965
+ return trimmed;
2966
+ });
2967
+ }
2968
+ return {
2969
+ column: field,
2970
+ operator: rebaseOp,
2971
+ value: parsedVal
2972
+ };
2973
+ }
2974
+ function parseLogicalList(str) {
2975
+ const list = [];
2976
+ let depth = 0;
2977
+ let current = "";
2978
+ for (let i = 0; i < str.length; i++) {
2979
+ const char = str[i];
2980
+ if (char === "(") depth++;
2981
+ if (char === ")") depth--;
2982
+ if (char === "," && depth === 0) {
2983
+ list.push(parseLogicalString(current));
2984
+ current = "";
2985
+ } else {
2986
+ current += char;
2987
+ }
2988
+ }
2989
+ if (current) {
2990
+ list.push(parseLogicalString(current));
2991
+ }
2992
+ return list;
2993
+ }
2920
2994
  function parseQueryOptions(query) {
2921
2995
  const options2 = {};
2922
- if (query.limit) options2.limit = parseInt(String(query.limit));
2923
- if (query.offset) options2.offset = parseInt(String(query.offset));
2924
- if (query.page) {
2925
- const page = parseInt(String(query.page));
2996
+ const limitVal = getLastValue(query.limit);
2997
+ if (limitVal) options2.limit = parseInt(String(limitVal));
2998
+ const offsetVal = getLastValue(query.offset);
2999
+ if (offsetVal) options2.offset = parseInt(String(offsetVal));
3000
+ const pageVal = getLastValue(query.page);
3001
+ if (pageVal) {
3002
+ const page = parseInt(String(pageVal));
2926
3003
  const limit = options2.limit || 20;
2927
3004
  options2.offset = (page - 1) * limit;
2928
3005
  }
2929
3006
  options2.where = {};
2930
- const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString", "vector_search", "vector", "vector_distance", "vector_threshold"];
3007
+ const orVal = getLastValue(query.or);
3008
+ const andVal = getLastValue(query.and);
3009
+ if (orVal) {
3010
+ let orStr = String(orVal).trim();
3011
+ if (orStr.startsWith("(") && orStr.endsWith(")")) {
3012
+ orStr = orStr.slice(1, -1);
3013
+ }
3014
+ options2.logical = {
3015
+ type: "or",
3016
+ conditions: parseLogicalList(orStr)
3017
+ };
3018
+ } else if (andVal) {
3019
+ let andStr = String(andVal).trim();
3020
+ if (andStr.startsWith("(") && andStr.endsWith(")")) {
3021
+ andStr = andStr.slice(1, -1);
3022
+ }
3023
+ options2.logical = {
3024
+ type: "and",
3025
+ conditions: parseLogicalList(andStr)
3026
+ };
3027
+ }
3028
+ const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString", "vector_search", "vector", "vector_distance", "vector_threshold", "or", "and"];
2931
3029
  for (const [key, rawValue] of Object.entries(query)) {
2932
3030
  if (reservedQueryKeys.includes(key)) continue;
2933
- const value = Array.isArray(rawValue) ? rawValue[rawValue.length - 1] : rawValue;
2934
- if (typeof value === "string") {
2935
- const parts = value.split(".");
2936
- if (parts.length >= 2) {
2937
- const op = parts[0];
2938
- const val = parts.slice(1).join(".");
2939
- const rebaseOp = mapOperator(op);
2940
- if (rebaseOp) {
2941
- let parsedVal = val;
2942
- if (val === "true") parsedVal = true;
2943
- else if (val === "false") parsedVal = false;
2944
- else if (val === "null") parsedVal = null;
2945
- else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
2946
- else if (val.startsWith("(")) {
2947
- const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
2948
- parsedVal = arrayContent.split(",").map((v) => {
2949
- const trimmed = v.trim();
2950
- if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
2951
- if (trimmed === "true") return true;
2952
- if (trimmed === "false") return false;
2953
- if (trimmed === "null") return null;
2954
- return trimmed;
2955
- });
3031
+ const rawValues = Array.isArray(rawValue) ? rawValue : [rawValue];
3032
+ const conditions = [];
3033
+ for (const value of rawValues) {
3034
+ if (typeof value === "string") {
3035
+ const parts = value.split(".");
3036
+ if (parts.length >= 2) {
3037
+ const op = parts[0];
3038
+ const val = parts.slice(1).join(".");
3039
+ const rebaseOp = mapOperator(op);
3040
+ if (rebaseOp) {
3041
+ let parsedVal = val;
3042
+ if (val === "true") parsedVal = true;
3043
+ else if (val === "false") parsedVal = false;
3044
+ else if (val === "null") parsedVal = null;
3045
+ else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
3046
+ else if (val.startsWith("(")) {
3047
+ const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
3048
+ parsedVal = arrayContent.split(",").map((v) => {
3049
+ const trimmed = v.trim();
3050
+ if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
3051
+ if (trimmed === "true") return true;
3052
+ if (trimmed === "false") return false;
3053
+ if (trimmed === "null") return null;
3054
+ return trimmed;
3055
+ });
3056
+ }
3057
+ conditions.push([rebaseOp, parsedVal]);
3058
+ } else {
3059
+ let parsedVal = value;
3060
+ if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
3061
+ conditions.push(["==", parsedVal]);
2956
3062
  }
2957
- options2.where[key] = [rebaseOp, parsedVal];
2958
3063
  } else {
2959
3064
  let parsedVal = value;
2960
- if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
2961
- options2.where[key] = ["==", parsedVal];
3065
+ if (value === "true") parsedVal = true;
3066
+ else if (value === "false") parsedVal = false;
3067
+ else if (value === "null") parsedVal = null;
3068
+ else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
3069
+ conditions.push(["==", parsedVal]);
2962
3070
  }
2963
- } else {
2964
- let parsedVal = value;
2965
- if (value === "true") parsedVal = true;
2966
- else if (value === "false") parsedVal = false;
2967
- else if (value === "null") parsedVal = null;
2968
- else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
2969
- options2.where[key] = ["==", parsedVal];
2970
3071
  }
2971
3072
  }
3073
+ if (conditions.length > 0) {
3074
+ options2.where[key] = conditions.length === 1 ? conditions[0] : conditions;
3075
+ }
2972
3076
  }
2973
3077
  if (Object.keys(options2.where).length === 0) {
2974
3078
  delete options2.where;
2975
3079
  }
2976
- if (query.orderBy) {
3080
+ const orderByVal = getLastValue(query.orderBy);
3081
+ if (orderByVal) {
2977
3082
  try {
2978
- options2.orderBy = typeof query.orderBy === "string" ? JSON.parse(query.orderBy) : query.orderBy;
3083
+ options2.orderBy = typeof orderByVal === "string" ? JSON.parse(orderByVal) : orderByVal;
2979
3084
  } catch {
2980
- if (typeof query.orderBy === "string") {
2981
- const [field, direction] = query.orderBy.split(":");
3085
+ if (typeof orderByVal === "string") {
3086
+ const [field, direction] = orderByVal.split(":");
2982
3087
  const dir = direction === "desc" ? "desc" : "asc";
2983
3088
  options2.orderBy = [{
2984
3089
  field,
@@ -2987,20 +3092,24 @@ function parseQueryOptions(query) {
2987
3092
  }
2988
3093
  }
2989
3094
  }
2990
- if (query.include) {
2991
- const includeStr = String(query.include).trim();
3095
+ const includeVal = getLastValue(query.include);
3096
+ if (includeVal) {
3097
+ const includeStr = String(includeVal).trim();
2992
3098
  if (includeStr === "*") {
2993
3099
  options2.include = ["*"];
2994
3100
  } else {
2995
3101
  options2.include = includeStr.split(",").map((s2) => s2.trim()).filter(Boolean);
2996
3102
  }
2997
3103
  }
2998
- if (query.fields) {
2999
- const fieldsStr = String(query.fields).trim();
3104
+ const fieldsVal = getLastValue(query.fields);
3105
+ if (fieldsVal) {
3106
+ const fieldsStr = String(fieldsVal).trim();
3000
3107
  options2.fields = fieldsStr.split(",").map((s2) => s2.trim()).filter(Boolean);
3001
3108
  }
3002
- if (query.vector_search && query.vector) {
3003
- const vectorStr = String(query.vector);
3109
+ const vectorSearchVal = getLastValue(query.vector_search);
3110
+ const vectorVal = getLastValue(query.vector);
3111
+ if (vectorSearchVal && vectorVal) {
3112
+ const vectorStr = String(vectorVal);
3004
3113
  let queryVector;
3005
3114
  try {
3006
3115
  queryVector = JSON.parse(vectorStr);
@@ -3010,17 +3119,19 @@ function parseQueryOptions(query) {
3010
3119
  } catch {
3011
3120
  throw new Error("Invalid vector format. Expected JSON array of numbers, e.g. [0.1,0.2,0.3]");
3012
3121
  }
3013
- const distanceParam = query.vector_distance ? String(query.vector_distance) : "cosine";
3122
+ const distanceParamVal = getLastValue(query.vector_distance);
3123
+ const distanceParam = distanceParamVal ? String(distanceParamVal) : "cosine";
3014
3124
  if (distanceParam !== "cosine" && distanceParam !== "l2" && distanceParam !== "inner_product") {
3015
3125
  throw new Error(`Invalid vector_distance: ${distanceParam}. Expected: cosine, l2, or inner_product`);
3016
3126
  }
3017
3127
  const vectorSearch = {
3018
- property: String(query.vector_search),
3128
+ property: String(vectorSearchVal),
3019
3129
  vector: queryVector,
3020
3130
  distance: distanceParam
3021
3131
  };
3022
- if (query.vector_threshold) {
3023
- const threshold = parseFloat(String(query.vector_threshold));
3132
+ const thresholdVal = getLastValue(query.vector_threshold);
3133
+ if (thresholdVal) {
3134
+ const threshold = parseFloat(String(thresholdVal));
3024
3135
  if (isNaN(threshold)) {
3025
3136
  throw new Error("Invalid vector_threshold. Expected a number.");
3026
3137
  }
@@ -3115,9 +3226,9 @@ class RestApiGenerator {
3115
3226
  const resolvedCollection = collection;
3116
3227
  this.router.get(`${basePath}/count`, async (c) => {
3117
3228
  this.enforceApiKeyPermission(c, collection.slug);
3118
- const queryDict = c.req.query();
3229
+ const queryDict = c.req.queries();
3119
3230
  const queryOptions = parseQueryOptions(queryDict);
3120
- const searchString = queryDict.searchString;
3231
+ const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : void 0;
3121
3232
  const driver = c.get("driver") || this.driver;
3122
3233
  const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
3123
3234
  return c.json({
@@ -3126,9 +3237,9 @@ class RestApiGenerator {
3126
3237
  });
3127
3238
  this.router.get(basePath, async (c) => {
3128
3239
  this.enforceApiKeyPermission(c, collection.slug);
3129
- const queryDict = c.req.query();
3240
+ const queryDict = c.req.queries();
3130
3241
  const queryOptions = parseQueryOptions(queryDict);
3131
- const searchString = queryDict.searchString;
3242
+ const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : void 0;
3132
3243
  const driver = c.get("driver") || this.driver;
3133
3244
  const fetchService = this.getFetchService(driver);
3134
3245
  const hookCtx = this.buildHookContext(c, "GET");
@@ -3171,7 +3282,7 @@ class RestApiGenerator {
3171
3282
  this.router.get(`${basePath}/:id`, async (c) => {
3172
3283
  this.enforceApiKeyPermission(c, collection.slug);
3173
3284
  const id = c.req.param("id");
3174
- const queryDict = c.req.query();
3285
+ const queryDict = c.req.queries();
3175
3286
  const queryOptions = parseQueryOptions(queryDict);
3176
3287
  const driver = c.get("driver") || this.driver;
3177
3288
  const fetchService = this.getFetchService(driver);
@@ -3222,9 +3333,10 @@ class RestApiGenerator {
3222
3333
  }
3223
3334
  return c.json(response, 201);
3224
3335
  } catch (error2) {
3225
- const err = error2;
3226
- err.code = err.code || "BAD_REQUEST";
3227
- throw err;
3336
+ if (isRebaseApiError(error2) && !error2.code) {
3337
+ error2.code = "BAD_REQUEST";
3338
+ }
3339
+ throw error2;
3228
3340
  }
3229
3341
  });
3230
3342
  this.router.put(`${basePath}/:id`, async (c) => {
@@ -3260,9 +3372,10 @@ class RestApiGenerator {
3260
3372
  }
3261
3373
  return c.json(response);
3262
3374
  } catch (error2) {
3263
- const err = error2;
3264
- err.code = err.code || "BAD_REQUEST";
3265
- throw err;
3375
+ if (isRebaseApiError(error2) && !error2.code) {
3376
+ error2.code = "BAD_REQUEST";
3377
+ }
3378
+ throw error2;
3266
3379
  }
3267
3380
  });
3268
3381
  this.router.delete(`${basePath}/:id`, async (c) => {
@@ -3339,13 +3452,15 @@ class RestApiGenerator {
3339
3452
  if (!parsed) return next();
3340
3453
  const driver = c.get("driver") || this.driver;
3341
3454
  this.enforceApiKeyPermission(c, c.req.param("parent"));
3455
+ const hookCtx = this.buildHookContext(c, "GET");
3342
3456
  if (parsed.entityId === "count") {
3343
- const queryDict = c.req.query();
3457
+ const queryDict = c.req.queries();
3344
3458
  const queryOptions = parseQueryOptions(queryDict);
3459
+ const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : void 0;
3345
3460
  const total = driver.countEntities ? await driver.countEntities({
3346
3461
  path: parsed.collectionPath,
3347
3462
  filter: queryOptions.where,
3348
- searchString: queryDict.searchString
3463
+ searchString
3349
3464
  }) : 0;
3350
3465
  return c.json({
3351
3466
  count: total
@@ -3356,25 +3471,36 @@ class RestApiGenerator {
3356
3471
  entityId: parsed.entityId
3357
3472
  });
3358
3473
  if (!entity) throw ApiError.notFound("Entity not found");
3359
- return c.json(this.flattenEntity(entity));
3474
+ const flatResult = this.flattenEntity(entity);
3475
+ const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
3476
+ if (!hookResult) throw ApiError.notFound("Entity not found");
3477
+ return c.json(hookResult);
3360
3478
  } else {
3361
- const queryDict = c.req.query();
3479
+ const queryDict = c.req.queries();
3362
3480
  const queryOptions = parseQueryOptions(queryDict);
3481
+ const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : void 0;
3363
3482
  const entities = await driver.fetchCollection({
3364
3483
  path: parsed.collectionPath,
3365
3484
  filter: queryOptions.where,
3366
3485
  limit: queryOptions.limit,
3367
3486
  orderBy: queryOptions.orderBy?.[0]?.field,
3368
3487
  order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
3369
- searchString: queryDict.searchString
3488
+ searchString
3370
3489
  });
3490
+ let flatEntities = entities.map((e) => this.flattenEntity(e));
3491
+ flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
3492
+ const total = driver.countEntities ? await driver.countEntities({
3493
+ path: parsed.collectionPath,
3494
+ filter: queryOptions.where,
3495
+ searchString
3496
+ }) : flatEntities.length;
3371
3497
  return c.json({
3372
- data: entities.map((e) => this.flattenEntity(e)),
3498
+ data: flatEntities,
3373
3499
  meta: {
3374
- total: entities.length,
3500
+ total,
3375
3501
  limit: queryOptions.limit,
3376
3502
  offset: queryOptions.offset,
3377
- hasMore: false
3503
+ hasMore: (queryOptions.offset || 0) + flatEntities.length < total
3378
3504
  }
3379
3505
  });
3380
3506
  }
@@ -3386,14 +3512,24 @@ class RestApiGenerator {
3386
3512
  const parsed = parseSubPath(rawPath);
3387
3513
  if (!parsed || parsed.entityId) return next();
3388
3514
  const driver = c.get("driver") || this.driver;
3515
+ const hookCtx = this.buildHookContext(c, "POST");
3389
3516
  this.enforceApiKeyPermission(c, c.req.param("parent"));
3390
- const body = await c.req.json().catch(() => ({}));
3517
+ let body = await c.req.json().catch(() => ({}));
3518
+ if (this.dataHooks?.beforeSave) {
3519
+ body = await this.dataHooks.beforeSave(parsed.collectionPath, body, void 0, hookCtx);
3520
+ }
3391
3521
  const entity = await driver.saveEntity({
3392
3522
  path: parsed.collectionPath,
3393
3523
  values: body,
3394
3524
  status: "new"
3395
3525
  });
3396
- return c.json(this.formatResponse(entity), 201);
3526
+ const response = this.formatResponse(entity);
3527
+ if (this.dataHooks?.afterSave) {
3528
+ Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response, hookCtx)).catch((err) => {
3529
+ console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
3530
+ });
3531
+ }
3532
+ return c.json(response, 201);
3397
3533
  });
3398
3534
  this.router.put("/:parent/:parentId/:rest{.+}", async (c, next) => {
3399
3535
  const rest = c.req.param("rest");
@@ -3402,15 +3538,25 @@ class RestApiGenerator {
3402
3538
  const parsed = parseSubPath(rawPath);
3403
3539
  if (!parsed || !parsed.entityId) return next();
3404
3540
  const driver = c.get("driver") || this.driver;
3541
+ const hookCtx = this.buildHookContext(c, "PUT");
3405
3542
  this.enforceApiKeyPermission(c, c.req.param("parent"));
3406
- const body = await c.req.json().catch(() => ({}));
3543
+ let body = await c.req.json().catch(() => ({}));
3544
+ if (this.dataHooks?.beforeSave) {
3545
+ body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
3546
+ }
3407
3547
  const entity = await driver.saveEntity({
3408
3548
  path: parsed.collectionPath,
3409
3549
  entityId: parsed.entityId,
3410
3550
  values: body,
3411
3551
  status: "existing"
3412
3552
  });
3413
- return c.json(this.formatResponse(entity));
3553
+ const response = this.formatResponse(entity);
3554
+ if (this.dataHooks?.afterSave) {
3555
+ Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response, hookCtx)).catch((err) => {
3556
+ console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
3557
+ });
3558
+ }
3559
+ return c.json(response);
3414
3560
  });
3415
3561
  this.router.delete("/:parent/:parentId/:rest{.+}", async (c, next) => {
3416
3562
  const rest = c.req.param("rest");
@@ -3419,15 +3565,24 @@ class RestApiGenerator {
3419
3565
  const parsed = parseSubPath(rawPath);
3420
3566
  if (!parsed || !parsed.entityId) return next();
3421
3567
  const driver = c.get("driver") || this.driver;
3568
+ const hookCtx = this.buildHookContext(c, "DELETE");
3422
3569
  this.enforceApiKeyPermission(c, c.req.param("parent"));
3423
3570
  const existingEntity = await driver.fetchEntity({
3424
3571
  path: parsed.collectionPath,
3425
3572
  entityId: parsed.entityId
3426
3573
  });
3427
3574
  if (!existingEntity) throw ApiError.notFound("Entity not found");
3575
+ if (this.dataHooks?.beforeDelete) {
3576
+ await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
3577
+ }
3428
3578
  await driver.deleteEntity({
3429
3579
  entity: existingEntity
3430
3580
  });
3581
+ if (this.dataHooks?.afterDelete) {
3582
+ Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch((err) => {
3583
+ console.error("[BackendHooks] data.afterDelete error:", err instanceof Error ? err.message : err);
3584
+ });
3585
+ }
3431
3586
  return new Response(null, {
3432
3587
  status: 204
3433
3588
  });
@@ -12910,14 +13065,11 @@ function createAuthRoutes(config) {
12910
13065
  if (!factor || factor.userId !== userCtx.userId) {
12911
13066
  throw ApiError.notFound("MFA factor not found");
12912
13067
  }
12913
- const isRecoveryCode = code2.length > 6;
12914
- let isValid2 = false;
12915
- if (isRecoveryCode) {
13068
+ const secretBuffer = base32Decode(factor.secretEncrypted);
13069
+ let isValid2 = verifyTotp(secretBuffer, code2);
13070
+ if (!isValid2) {
12916
13071
  const codeHash = hashRecoveryCode(code2);
12917
13072
  isValid2 = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
12918
- } else {
12919
- const secretBuffer = base32Decode(factor.secretEncrypted);
12920
- isValid2 = verifyTotp(secretBuffer, code2);
12921
13073
  }
12922
13074
  if (!isValid2) {
12923
13075
  throw ApiError.unauthorized("Invalid verification code", "INVALID_CODE");
@@ -13035,11 +13187,6 @@ function createAdminRoutes(config) {
13035
13187
  const results = await Promise.all(users.map((u) => applyUserAfterRead(u, ctx)));
13036
13188
  return results.filter((u) => u !== null);
13037
13189
  }
13038
- async function applyRoleAfterReadBatch(roles, ctx) {
13039
- if (!hooks?.roles?.afterRead) return roles;
13040
- const results = await Promise.all(roles.map((r) => hooks.roles.afterRead(r, ctx)));
13041
- return results.filter((r) => r !== null);
13042
- }
13043
13190
  function toAdminUser(u, roles) {
13044
13191
  return {
13045
13192
  uid: u.id,
@@ -13398,90 +13545,6 @@ function createAdminRoutes(config) {
13398
13545
  success: true
13399
13546
  });
13400
13547
  });
13401
- router.get("/roles", requireAdmin, async (c) => {
13402
- const roles = await authRepo.listRoles();
13403
- const hookCtx = buildHookContext(c, "GET");
13404
- let adminRoles = roles.map((r) => ({
13405
- id: r.id,
13406
- name: r.name,
13407
- isAdmin: r.isAdmin,
13408
- defaultPermissions: r.defaultPermissions
13409
- }));
13410
- adminRoles = await applyRoleAfterReadBatch(adminRoles, hookCtx);
13411
- return c.json({
13412
- roles: adminRoles
13413
- });
13414
- });
13415
- router.get("/roles/:roleId", requireAdmin, async (c) => {
13416
- const roleId = c.req.param("roleId");
13417
- const role = await authRepo.getRoleById(roleId);
13418
- if (!role) {
13419
- throw ApiError.notFound("Role not found");
13420
- }
13421
- return c.json({
13422
- role
13423
- });
13424
- });
13425
- router.post("/roles", requireAdmin, async (c) => {
13426
- const body = await c.req.json();
13427
- const {
13428
- id,
13429
- name: name2,
13430
- isAdmin,
13431
- defaultPermissions
13432
- } = body;
13433
- if (!id || !name2) {
13434
- throw ApiError.badRequest("Role ID and name are required", "INVALID_INPUT");
13435
- }
13436
- const existing = await authRepo.getRoleById(id);
13437
- if (existing) {
13438
- throw ApiError.conflict("Role already exists", "ROLE_EXISTS");
13439
- }
13440
- const role = await authRepo.createRole({
13441
- id,
13442
- name: name2,
13443
- isAdmin: isAdmin ?? false,
13444
- defaultPermissions: defaultPermissions ?? null
13445
- });
13446
- return c.json({
13447
- role
13448
- }, 201);
13449
- });
13450
- router.put("/roles/:roleId", requireAdmin, async (c) => {
13451
- const roleId = c.req.param("roleId");
13452
- const body = await c.req.json();
13453
- const {
13454
- name: name2,
13455
- isAdmin,
13456
- defaultPermissions
13457
- } = body;
13458
- const existing = await authRepo.getRoleById(roleId);
13459
- if (!existing) {
13460
- throw ApiError.notFound("Role not found");
13461
- }
13462
- const role = await authRepo.updateRole(roleId, {
13463
- name: name2,
13464
- isAdmin,
13465
- defaultPermissions
13466
- });
13467
- return c.json({
13468
- role
13469
- });
13470
- });
13471
- router.delete("/roles/:roleId", requireAdmin, async (c) => {
13472
- const roleId = c.req.param("roleId");
13473
- if (["admin", "editor", "viewer"].includes(roleId)) {
13474
- throw ApiError.badRequest("Cannot delete built-in roles", "BUILTIN_ROLE");
13475
- }
13476
- const existing = await authRepo.getRoleById(roleId);
13477
- if (!existing) {
13478
- throw ApiError.notFound("Role not found");
13479
- }
13480
- await authRepo.deleteRole(roleId);
13481
- return c.json({
13482
- success: true
13483
- });
13484
- });
13485
13548
  return router;
13486
13549
  }
13487
13550
  function createBuiltinAuthAdapter(config) {
@@ -13522,18 +13585,16 @@ function createBuiltinAuthAdapter(config) {
13522
13585
  if (!payload) {
13523
13586
  return null;
13524
13587
  }
13525
- const extendedPayload = payload;
13526
13588
  let roles = payload.roles || [];
13527
13589
  try {
13528
- const userRoles = await authRepository.getUserRoles(payload.userId);
13529
- roles = userRoles.map((r) => r.id);
13590
+ roles = await authRepository.getUserRoleIds(payload.userId);
13530
13591
  } catch {
13531
13592
  }
13532
13593
  const isAdmin = roles.some((r) => r === "admin" || r === "schema-admin");
13533
13594
  return {
13534
13595
  uid: payload.userId,
13535
- email: extendedPayload.email ?? "",
13536
- displayName: extendedPayload.displayName ?? null,
13596
+ email: payload.email ?? "",
13597
+ displayName: payload.displayName ?? null,
13537
13598
  roles,
13538
13599
  isAdmin,
13539
13600
  rawToken: token
@@ -13553,25 +13614,22 @@ function createBuiltinAuthAdapter(config) {
13553
13614
  if (!payload) {
13554
13615
  return null;
13555
13616
  }
13556
- const extendedPayload = payload;
13557
13617
  let roles = payload.roles || [];
13558
13618
  try {
13559
- const userRoles = await authRepository.getUserRoles(payload.userId);
13560
- roles = userRoles.map((r) => r.id);
13619
+ roles = await authRepository.getUserRoleIds(payload.userId);
13561
13620
  } catch {
13562
13621
  }
13563
13622
  const isAdmin = roles.some((r) => r === "admin" || r === "schema-admin");
13564
13623
  return {
13565
13624
  uid: payload.userId,
13566
- email: extendedPayload.email ?? "",
13567
- displayName: extendedPayload.displayName ?? null,
13625
+ email: payload.email ?? "",
13626
+ displayName: payload.displayName ?? null,
13568
13627
  roles,
13569
13628
  isAdmin,
13570
13629
  rawToken: token
13571
13630
  };
13572
13631
  },
13573
13632
  userManagement: createUserManagementFromRepo(authRepository, resolvedOps, authHooks),
13574
- roleManagement: createRoleManagementFromRepo(authRepository),
13575
13633
  createAuthRoutes() {
13576
13634
  return createAuthRoutes({
13577
13635
  authRepo: authRepository,
@@ -13687,43 +13745,13 @@ function createUserManagementFromRepo(repo, resolvedOps, authHooks) {
13687
13745
  }
13688
13746
  },
13689
13747
  async getUserRoles(userId) {
13690
- const roles = await repo.getUserRoles(userId);
13691
- return roles.map(toAuthRoleData);
13748
+ return repo.getUserRoleIds(userId);
13692
13749
  },
13693
13750
  async setUserRoles(userId, roleIds) {
13694
13751
  await repo.setUserRoles(userId, roleIds);
13695
13752
  }
13696
13753
  };
13697
13754
  }
13698
- function createRoleManagementFromRepo(repo) {
13699
- return {
13700
- async listRoles() {
13701
- const roles = await repo.listRoles();
13702
- return roles.map(toAuthRoleData);
13703
- },
13704
- async getRoleById(id) {
13705
- const role = await repo.getRoleById(id);
13706
- return role ? toAuthRoleData(role) : null;
13707
- },
13708
- async createRole(data) {
13709
- const role = await repo.createRole({
13710
- id: data.id,
13711
- name: data.name,
13712
- isAdmin: data.isAdmin,
13713
- defaultPermissions: data.defaultPermissions,
13714
- collectionPermissions: data.collectionPermissions
13715
- });
13716
- return toAuthRoleData(role);
13717
- },
13718
- async updateRole(id, data) {
13719
- const role = await repo.updateRole(id, data);
13720
- return role ? toAuthRoleData(role) : null;
13721
- },
13722
- async deleteRole(id) {
13723
- await repo.deleteRole(id);
13724
- }
13725
- };
13726
- }
13727
13755
  function toAuthUserData(user) {
13728
13756
  return {
13729
13757
  id: user.id,
@@ -13736,15 +13764,6 @@ function toAuthUserData(user) {
13736
13764
  updatedAt: user.updatedAt
13737
13765
  };
13738
13766
  }
13739
- function toAuthRoleData(role) {
13740
- return {
13741
- id: role.id,
13742
- name: role.name,
13743
- isAdmin: role.isAdmin,
13744
- defaultPermissions: role.defaultPermissions,
13745
- collectionPermissions: role.collectionPermissions
13746
- };
13747
- }
13748
13767
  function configureLogLevel(logLevel) {
13749
13768
  const LOG_LEVEL = logLevel || process.env.LOG_LEVEL || "info";
13750
13769
  const logLevels = {
@@ -24927,7 +24946,6 @@ function createCustomAuthAdapter(options2) {
24927
24946
  verifyRequest: options2.verifyRequest,
24928
24947
  verifyToken: resolvedVerifyToken,
24929
24948
  userManagement: options2.userManagement,
24930
- roleManagement: options2.roleManagement,
24931
24949
  getCapabilities() {
24932
24950
  return defaultCapabilities;
24933
24951
  }
@@ -26225,15 +26243,33 @@ function normalizeWhereValue(value) {
26225
26243
  if (value === null) return "eq.null";
26226
26244
  if (typeof value === "boolean") return `eq.${value}`;
26227
26245
  if (typeof value === "number") return String(value);
26228
- if (Array.isArray(value) && value.length === 2) {
26229
- const [rawOp, val] = value;
26230
- const op = OP_MAP[rawOp] ?? rawOp;
26231
- if (val === null) return `${op}.null`;
26232
- if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
26233
- return `${op}.${val}`;
26246
+ if (Array.isArray(value)) {
26247
+ const conditions = Array.isArray(value[0]) ? value : [value];
26248
+ const [rawOp, val] = conditions[0] || [];
26249
+ if (rawOp) {
26250
+ const op = OP_MAP[rawOp] ?? rawOp;
26251
+ if (val === null) return `${op}.null`;
26252
+ if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
26253
+ return `${op}.${val}`;
26254
+ }
26234
26255
  }
26235
26256
  return String(value);
26236
26257
  }
26258
+ function serializeLogicalCondition(cond2) {
26259
+ if ("type" in cond2) {
26260
+ const sub = cond2.conditions.map(serializeLogicalCondition).join(",");
26261
+ return `${cond2.type}(${sub})`;
26262
+ } else {
26263
+ const op = OP_MAP[cond2.operator] ?? cond2.operator;
26264
+ let formattedValue = cond2.value;
26265
+ if (Array.isArray(cond2.value)) {
26266
+ formattedValue = `(${cond2.value.join(",")})`;
26267
+ } else if (cond2.value === null) {
26268
+ formattedValue = "null";
26269
+ }
26270
+ return `${cond2.column}.${op}.${formattedValue}`;
26271
+ }
26272
+ }
26237
26273
  function buildQueryString(params) {
26238
26274
  if (!params) return "";
26239
26275
  const parts = [];
@@ -26249,10 +26285,22 @@ function buildQueryString(params) {
26249
26285
  if (params.include && params.include.length > 0) {
26250
26286
  parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
26251
26287
  }
26288
+ if (params.logical) {
26289
+ const root = params.logical;
26290
+ const serialized = root.conditions.map(serializeLogicalCondition).join(",");
26291
+ parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
26292
+ }
26252
26293
  if (params.where) {
26253
26294
  for (const [field, value] of Object.entries(params.where)) {
26254
- const normalized2 = normalizeWhereValue(value);
26255
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized2)}`);
26295
+ if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
26296
+ for (const subVal of value) {
26297
+ const normalized2 = normalizeWhereValue(subVal);
26298
+ parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized2)}`);
26299
+ }
26300
+ } else {
26301
+ const normalized2 = normalizeWhereValue(value);
26302
+ parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized2)}`);
26303
+ }
26256
26304
  }
26257
26305
  }
26258
26306
  return parts.length > 0 ? "?" + parts.join("&") : "";
@@ -26927,33 +26975,6 @@ function createAdmin(transport, options2) {
26927
26975
  method: "DELETE"
26928
26976
  });
26929
26977
  }
26930
- async function listRoles() {
26931
- return transport.request(adminPath + "/roles", {
26932
- method: "GET"
26933
- });
26934
- }
26935
- async function getRole(roleId) {
26936
- return transport.request(adminPath + "/roles/" + encodeURIComponent(roleId), {
26937
- method: "GET"
26938
- });
26939
- }
26940
- async function createRole(data) {
26941
- return transport.request(adminPath + "/roles", {
26942
- method: "POST",
26943
- body: JSON.stringify(data)
26944
- });
26945
- }
26946
- async function updateRole(roleId, data) {
26947
- return transport.request(adminPath + "/roles/" + encodeURIComponent(roleId), {
26948
- method: "PUT",
26949
- body: JSON.stringify(data)
26950
- });
26951
- }
26952
- async function deleteRole(roleId) {
26953
- return transport.request(adminPath + "/roles/" + encodeURIComponent(roleId), {
26954
- method: "DELETE"
26955
- });
26956
- }
26957
26978
  async function bootstrap() {
26958
26979
  return transport.request(adminPath + "/bootstrap", {
26959
26980
  method: "POST"
@@ -26966,11 +26987,6 @@ function createAdmin(transport, options2) {
26966
26987
  createUser,
26967
26988
  updateUser,
26968
26989
  deleteUser,
26969
- listRoles,
26970
- getRole,
26971
- createRole,
26972
- updateRole,
26973
- deleteRole,
26974
26990
  bootstrap
26975
26991
  };
26976
26992
  }
@@ -27036,11 +27052,18 @@ function createCollectionClient(transport, slug, ws) {
27036
27052
  };
27037
27053
  },
27038
27054
  async findById(id) {
27039
- const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
27040
- method: "GET"
27041
- });
27042
- if (!raw) return void 0;
27043
- return rowToEntity(raw, slug);
27055
+ try {
27056
+ const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
27057
+ method: "GET"
27058
+ });
27059
+ if (!raw) return void 0;
27060
+ return rowToEntity(raw, slug);
27061
+ } catch (err) {
27062
+ if (err instanceof RebaseApiError && err.status === 404) {
27063
+ return void 0;
27064
+ }
27065
+ throw err;
27066
+ }
27044
27067
  },
27045
27068
  async create(data, id) {
27046
27069
  const body = {
@@ -27080,8 +27103,12 @@ function createCollectionClient(transport, slug, ws) {
27080
27103
  return raw.count ?? 0;
27081
27104
  },
27082
27105
  // Fluent builder instantiation
27083
- where(column, operator, value) {
27084
- return new QueryBuilder(client).where(column, operator, value);
27106
+ where(columnOrCondition, operator, value) {
27107
+ const builder = new QueryBuilder(client);
27108
+ if (typeof columnOrCondition === "object") {
27109
+ return builder.where(columnOrCondition);
27110
+ }
27111
+ return builder.where(columnOrCondition, operator, value);
27085
27112
  },
27086
27113
  orderBy(column, ascending) {
27087
27114
  return new QueryBuilder(client).orderBy(column, ascending);
@@ -38359,13 +38386,35 @@ var createTransport = function(transporter, defaults) {
38359
38386
  mailer2 = new Mailer(transporter, options2, defaults);
38360
38387
  return mailer2;
38361
38388
  };
38389
+ function getHostname(urlStr) {
38390
+ try {
38391
+ const url = new URL(urlStr.includes("://") ? urlStr : `https://${urlStr}`);
38392
+ return url.hostname;
38393
+ } catch {
38394
+ return void 0;
38395
+ }
38396
+ }
38362
38397
  class SMTPEmailService {
38363
38398
  transporter = null;
38364
38399
  config;
38365
38400
  constructor(config) {
38366
38401
  this.config = config;
38367
38402
  if (config.smtp) {
38403
+ let smtpName = config.smtp.name;
38404
+ if (!smtpName) {
38405
+ const urlsToTry = [process.env.FRONTEND_URL, config.resetPasswordUrl, config.verifyEmailUrl];
38406
+ for (const urlStr of urlsToTry) {
38407
+ if (urlStr) {
38408
+ const hostname = getHostname(urlStr);
38409
+ if (hostname) {
38410
+ smtpName = hostname;
38411
+ break;
38412
+ }
38413
+ }
38414
+ }
38415
+ }
38368
38416
  this.transporter = createTransport({
38417
+ name: smtpName,
38369
38418
  host: config.smtp.host,
38370
38419
  port: config.smtp.port,
38371
38420
  secure: config.smtp.secure ?? config.smtp.port === 465,
@@ -38515,6 +38564,14 @@ async function _initializeRebaseBackend(config) {
38515
38564
  dir: config.collectionsDir
38516
38565
  });
38517
38566
  }
38567
+ if (config.defaultSecurityRules?.length) {
38568
+ for (const collection of activeCollections) {
38569
+ if (isPostgresCollection(collection) && (!collection.securityRules || collection.securityRules.length === 0)) {
38570
+ collection.securityRules = config.defaultSecurityRules;
38571
+ }
38572
+ }
38573
+ logger.info("Default security rules applied to collections without explicit rules");
38574
+ }
38518
38575
  const realtimeServices = {};
38519
38576
  const delegates = {};
38520
38577
  let bootstrappers = config.bootstrappers || [];
@@ -38558,7 +38615,9 @@ async function _initializeRebaseBackend(config) {
38558
38615
  }
38559
38616
  if (bootstrapper.initializeRealtime) {
38560
38617
  const realtime = await bootstrapper.initializeRealtime({}, driverResult);
38561
- realtimeServices[b.id || bootstrapper.type] = realtime;
38618
+ if (realtime) {
38619
+ realtimeServices[b.id || bootstrapper.type] = realtime;
38620
+ }
38562
38621
  }
38563
38622
  }
38564
38623
  const driverRegistry = DefaultDriverRegistry.create(delegates);
@@ -38583,8 +38642,7 @@ async function _initializeRebaseBackend(config) {
38583
38642
  id: authAdapter.id
38584
38643
  });
38585
38644
  authConfigResult = {
38586
- userService: authAdapter.userManagement ?? {},
38587
- roleService: authAdapter.roleManagement ?? {}
38645
+ userService: authAdapter.userManagement ?? {}
38588
38646
  };
38589
38647
  } else {
38590
38648
  const safeAuthConfig = config.auth;
@@ -38929,6 +38987,26 @@ async function _initializeRebaseBackend(config) {
38929
38987
  logger.info("Email service attached to singleton", {
38930
38988
  configured: emailService.isConfigured()
38931
38989
  });
38990
+ if (emailService.isConfigured() && typeof emailService.verifyConnection === "function") {
38991
+ emailService.verifyConnection().then((success) => {
38992
+ if (!success) {
38993
+ logger.warn("Warning: SMTP connection verification failed. Email delivery may fail.");
38994
+ } else {
38995
+ logger.info("SMTP connection verified successfully.");
38996
+ }
38997
+ }).catch((err) => {
38998
+ logger.warn("Warning: SMTP connection verification failed. Email delivery may fail.", {
38999
+ error: err
39000
+ });
39001
+ });
39002
+ }
39003
+ }
39004
+ const driverAdmin = defaultBootstrapper.getAdmin?.(defaultDriverResult);
39005
+ if (isSQLAdmin(driverAdmin)) {
39006
+ Object.assign(serverClient, {
39007
+ sql: (query, options2) => driverAdmin.executeSql(query, options2)
39008
+ });
39009
+ logger.info("SQL capability attached to singleton");
38932
39010
  }
38933
39011
  _initRebase(serverClient);
38934
39012
  logger.info("Rebase singleton initialized");
@@ -39018,7 +39096,7 @@ async function _initializeRebaseBackend(config) {
39018
39096
  });
39019
39097
  }
39020
39098
  }
39021
- if (defaultBootstrapper.initializeWebsockets) {
39099
+ if (defaultBootstrapper.initializeWebsockets && defaultRealtimeService) {
39022
39100
  await defaultBootstrapper.initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
39023
39101
  }
39024
39102
  logger.info("Rebase Backend Initialized");
@@ -39064,12 +39142,11 @@ async function _initializeRebaseBackend(config) {
39064
39142
  }
39065
39143
  for (const [key, rt] of Object.entries(realtimeServices)) {
39066
39144
  try {
39067
- const rtWithLifecycle = rt;
39068
- if (typeof rtWithLifecycle.destroy === "function") {
39069
- await rtWithLifecycle.destroy();
39145
+ if (typeof rt.destroy === "function") {
39146
+ await rt.destroy();
39070
39147
  logger.info(`Realtime service "${key}" destroyed`);
39071
- } else if (typeof rtWithLifecycle.stopListening === "function") {
39072
- await rtWithLifecycle.stopListening();
39148
+ } else if (typeof rt.stopListening === "function") {
39149
+ await rt.stopListening();
39073
39150
  logger.info(`Realtime service "${key}" LISTEN client stopped`);
39074
39151
  }
39075
39152
  } catch (err) {
@@ -51216,6 +51293,7 @@ export {
51216
51293
  isAuthAdapter,
51217
51294
  isDatabaseAdapter,
51218
51295
  isOperationAllowed,
51296
+ isRebaseApiError,
51219
51297
  isTransformableImage,
51220
51298
  listenWithPortRetry,
51221
51299
  loadCronJobsFromDirectory,