@rebasepro/common 0.17.3 → 0.18.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.
- package/README.md +4 -0
- package/dist/collections/CollectionRegistry.d.ts +1 -1
- package/dist/collections/default-collections.d.ts +15 -84
- package/dist/data/buildRebaseData.d.ts +1 -1
- package/dist/data/filter-dialect.d.ts +11 -0
- package/dist/data/sort-dialect.d.ts +15 -3
- package/dist/index.es.js +375 -63
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +69 -24
- package/dist/util/callback-errors.d.ts +77 -0
- package/dist/util/callback-errors.test.d.ts +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/policy/evaluatePolicy.d.ts +6 -0
- package/dist/util/relations.d.ts +41 -0
- package/dist/util/table-name.test.d.ts +1 -0
- package/package.json +26 -22
- package/src/collections/CollectionRegistry.ts +0 -485
- package/src/collections/default-collections.ts +0 -109
- package/src/collections/index.ts +0 -2
- package/src/data/buildRebaseData.ts +0 -816
- package/src/data/buildRoutedRebaseData.ts +0 -103
- package/src/data/filter-conditions.ts +0 -46
- package/src/data/filter-dialect.ts +0 -737
- package/src/data/paginate.ts +0 -334
- package/src/data/query_builder.ts +0 -176
- package/src/data/resolveDataSource.ts +0 -135
- package/src/data/sort-dialect.ts +0 -237
- package/src/index.ts +0 -11
- package/src/table-classification.ts +0 -109
- package/src/types/json-logic-js.d.ts +0 -8
- package/src/util/auth-default-policies.ts +0 -215
- package/src/util/builders.ts +0 -82
- package/src/util/callbacks.ts +0 -122
- package/src/util/collections.ts +0 -117
- package/src/util/common.ts +0 -2
- package/src/util/conditions.ts +0 -168
- package/src/util/email.ts +0 -32
- package/src/util/entities.ts +0 -282
- package/src/util/enums.ts +0 -26
- package/src/util/identity.ts +0 -202
- package/src/util/index.ts +0 -21
- package/src/util/internal-tables.test.ts +0 -188
- package/src/util/internal-tables.ts +0 -197
- package/src/util/junction-policies.ts +0 -355
- package/src/util/paths.ts +0 -27
- package/src/util/permissions.test.ts +0 -866
- package/src/util/permissions.ts +0 -206
- package/src/util/pg-column-to-property.ts +0 -377
- package/src/util/policy/evaluatePolicy.ts +0 -194
- package/src/util/policy/index.ts +0 -4
- package/src/util/policy/policyToPostgres.ts +0 -263
- package/src/util/policy/securityRuleToConditions.ts +0 -67
- package/src/util/policy/sqlToPolicy.ts +0 -422
- package/src/util/relations.ts +0 -236
- package/src/util/resolutions.ts +0 -534
- package/src/util/resolve-relation.ts +0 -243
- package/src/util/storage.ts +0 -177
- package/src/util/string-column-length.ts +0 -31
package/src/util/callbacks.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { CollectionCallbacks, Properties, RebaseCallContext } from "@rebasepro/types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Context passed to entity lifecycle callbacks.
|
|
5
|
-
* @group Models
|
|
6
|
-
*/
|
|
7
|
-
export type EntityCallbackContext = RebaseCallContext;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Helper function to recursively check if there are any callbacks in the properties.
|
|
12
|
-
*/
|
|
13
|
-
function hasPropertyCallbacks(properties: Properties, callbackName: "afterRead" | "beforeSave"): boolean {
|
|
14
|
-
if (!properties) return false;
|
|
15
|
-
for (const property of Object.values(properties)) {
|
|
16
|
-
if (property.callbacks?.[callbackName]) return true;
|
|
17
|
-
if (property.type === "map" && property.properties) {
|
|
18
|
-
if (hasPropertyCallbacks(property.properties, callbackName)) return true;
|
|
19
|
-
} else if (property.type === "array" && property.of) {
|
|
20
|
-
const ofs = Array.isArray(property.of) ? property.of : [property.of];
|
|
21
|
-
for (const of of ofs) {
|
|
22
|
-
if (of.callbacks?.[callbackName]) return true;
|
|
23
|
-
if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Recursively process properties to apply field-level hooks.
|
|
32
|
-
*/
|
|
33
|
-
async function processProperties(
|
|
34
|
-
properties: Properties,
|
|
35
|
-
values: Record<string, unknown>,
|
|
36
|
-
previousValues: Record<string, unknown>,
|
|
37
|
-
propsContext: unknown,
|
|
38
|
-
callbackName: "afterRead" | "beforeSave"
|
|
39
|
-
): Promise<Record<string, unknown>> {
|
|
40
|
-
if (!values || typeof values !== "object") return values;
|
|
41
|
-
|
|
42
|
-
const result = { ...values };
|
|
43
|
-
|
|
44
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
45
|
-
if (result[key] === undefined) continue;
|
|
46
|
-
|
|
47
|
-
let currentValue = result[key];
|
|
48
|
-
const previousValue = previousValues?.[key];
|
|
49
|
-
|
|
50
|
-
// 1. Array Property
|
|
51
|
-
if (property.type === "array" && Array.isArray(currentValue)) {
|
|
52
|
-
// We only support traversing single-type arrays for hooks currently to avoid complex union matching
|
|
53
|
-
if (property.of && !Array.isArray(property.of)) {
|
|
54
|
-
currentValue = await Promise.all(currentValue.map(async (item, index) => {
|
|
55
|
-
const prevItem = Array.isArray(previousValue) ? previousValue[index] : undefined;
|
|
56
|
-
// Mock a properties object to process a single item
|
|
57
|
-
const singlePropData = { "_tmp": property.of } as Properties;
|
|
58
|
-
const res = await processProperties(singlePropData, { "_tmp": item }, { "_tmp": prevItem }, propsContext, callbackName);
|
|
59
|
-
return res["_tmp"];
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// 2. Map Property
|
|
64
|
-
else if (property.type === "map" && property.properties && typeof currentValue === "object") {
|
|
65
|
-
currentValue = await processProperties(property.properties, currentValue as Record<string, unknown>, (previousValue ?? {}) as Record<string, unknown>, propsContext, callbackName);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 3. Property's own callback
|
|
69
|
-
if (property.callbacks?.[callbackName]) {
|
|
70
|
-
|
|
71
|
-
const cbRes = await Promise.resolve(property.callbacks[callbackName]({
|
|
72
|
-
...(propsContext as Record<string, unknown>),
|
|
73
|
-
value: currentValue,
|
|
74
|
-
previousValue
|
|
75
|
-
} as never));
|
|
76
|
-
if (cbRes !== undefined) {
|
|
77
|
-
currentValue = cbRes;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
result[key] = currentValue;
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Helper function to extract field-level PropertyCallbacks from a properties schema
|
|
88
|
-
* and wrap them into an CollectionCallbacks object recursively.
|
|
89
|
-
*/
|
|
90
|
-
export const buildPropertyCallbacks = (properties: Properties): CollectionCallbacks | undefined => {
|
|
91
|
-
if (!properties) return undefined;
|
|
92
|
-
|
|
93
|
-
const propertyCallbacks: CollectionCallbacks = {};
|
|
94
|
-
|
|
95
|
-
if (hasPropertyCallbacks(properties, "afterRead")) {
|
|
96
|
-
propertyCallbacks.afterRead = async (props) => {
|
|
97
|
-
const row = props.row;
|
|
98
|
-
const processedValues = await processProperties(
|
|
99
|
-
properties,
|
|
100
|
-
row,
|
|
101
|
-
row,
|
|
102
|
-
props as unknown,
|
|
103
|
-
"afterRead"
|
|
104
|
-
);
|
|
105
|
-
return { ...props.row, ...processedValues };
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (hasPropertyCallbacks(properties, "beforeSave")) {
|
|
110
|
-
propertyCallbacks.beforeSave = async (props) => {
|
|
111
|
-
return await processProperties(
|
|
112
|
-
properties,
|
|
113
|
-
props.values as Record<string, unknown>,
|
|
114
|
-
(props.previousValues ?? {}) as Record<string, unknown>,
|
|
115
|
-
props as unknown,
|
|
116
|
-
"beforeSave"
|
|
117
|
-
);
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : undefined;
|
|
122
|
-
};
|
package/src/util/collections.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CollectionConfig,
|
|
3
|
-
Properties,
|
|
4
|
-
Property
|
|
5
|
-
} from "@rebasepro/types";
|
|
6
|
-
import { isPropertyBuilder } from "./entities";
|
|
7
|
-
|
|
8
|
-
export function sortProperties<M extends Record<string, unknown>>(properties: Properties, propertiesOrder?: string[]): Properties {
|
|
9
|
-
try {
|
|
10
|
-
const propertiesKeys = Object.keys(properties);
|
|
11
|
-
// If no propertiesOrder, just use the original keys order
|
|
12
|
-
if (!propertiesOrder || propertiesOrder.length === 0) {
|
|
13
|
-
return propertiesKeys
|
|
14
|
-
.map((key) => {
|
|
15
|
-
const property = properties[key] as Property;
|
|
16
|
-
if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
|
|
17
|
-
return ({
|
|
18
|
-
[key]: {
|
|
19
|
-
...property,
|
|
20
|
-
properties: sortProperties(property.properties, property.propertiesOrder)
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
} else {
|
|
24
|
-
return ({ [key]: property });
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
.reduce((a: Properties, b: Properties) => ({ ...a,
|
|
28
|
-
...b }), {}) as Properties;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Filter propertiesOrder to only include TOP-LEVEL property keys that exist
|
|
32
|
-
// (ignore nested keys like "data.mode" - they are for column ordering, not property filtering)
|
|
33
|
-
const validOrderKeys = (propertiesOrder as string[]).filter(key => {
|
|
34
|
-
// Only include top-level keys (no dots) that exist in properties
|
|
35
|
-
return !key.includes(".") && properties[key];
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Track which properties we've processed
|
|
39
|
-
const processedKeys = new Set<string>(validOrderKeys);
|
|
40
|
-
|
|
41
|
-
// Build result starting with ordered properties
|
|
42
|
-
const orderedResult = validOrderKeys
|
|
43
|
-
.map((key) => {
|
|
44
|
-
const property = properties[key] as Property;
|
|
45
|
-
if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
|
|
46
|
-
return ({
|
|
47
|
-
[key]: {
|
|
48
|
-
...property,
|
|
49
|
-
properties: sortProperties(property.properties, property.propertiesOrder)
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
} else {
|
|
53
|
-
return ({ [key]: property });
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
.reduce((a: Properties, b: Properties) => ({ ...a,
|
|
57
|
-
...b }), {}) as Properties;
|
|
58
|
-
|
|
59
|
-
// Append any properties that were NOT in propertiesOrder (so they don't disappear!)
|
|
60
|
-
const missingProperties = propertiesKeys
|
|
61
|
-
.filter(key => !processedKeys.has(key))
|
|
62
|
-
.map((key) => {
|
|
63
|
-
const property = properties[key] as Property;
|
|
64
|
-
if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
|
|
65
|
-
return ({
|
|
66
|
-
[key]: {
|
|
67
|
-
...property,
|
|
68
|
-
properties: sortProperties(property.properties, property.propertiesOrder)
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
} else {
|
|
72
|
-
return ({ [key]: property });
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
.reduce((a: Properties, b: Properties) => ({ ...a,
|
|
76
|
-
...b }), {}) as Properties;
|
|
77
|
-
|
|
78
|
-
return { ...orderedResult,
|
|
79
|
-
...missingProperties };
|
|
80
|
-
} catch (e) {
|
|
81
|
-
console.error("Error sorting properties", e);
|
|
82
|
-
return properties;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* A copy of `collections` ordered by slug.
|
|
88
|
-
*
|
|
89
|
-
* Every generator that turns collections into a file is order-dependent, and
|
|
90
|
-
* every one of them is compared against its own output — `rebase doctor`
|
|
91
|
-
* regenerates in memory and diffs, `generate-sdk && git diff --exit-code` gates
|
|
92
|
-
* CI. While only the *writers* sorted, a project whose `readdirSync` order
|
|
93
|
-
* differed from its slug order was reported permanently out of date, and the
|
|
94
|
-
* fix the message printed rewrote the file in the order it was already in. The
|
|
95
|
-
* generators sort themselves now, so no caller can get this wrong.
|
|
96
|
-
*
|
|
97
|
-
* A slug-less collection is left to the generator's own validation, which names
|
|
98
|
-
* the offending collection; sorting must not throw first.
|
|
99
|
-
*/
|
|
100
|
-
export function sortCollectionsBySlug<C extends { slug?: string }>(collections: readonly C[]): C[] {
|
|
101
|
-
return [...collections].sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function getPrimaryKeys<M extends Record<string, unknown>>(collection: CollectionConfig<M>): Extract<keyof M, string>[] {
|
|
105
|
-
const properties = collection.properties;
|
|
106
|
-
if (!properties) {
|
|
107
|
-
return ["id"] as Extract<keyof M, string>[];
|
|
108
|
-
}
|
|
109
|
-
const ids = Object.entries(properties)
|
|
110
|
-
.filter(([key, prop]) => typeof prop === "object" && prop !== null && "isId" in prop && Boolean(prop.isId))
|
|
111
|
-
.map(([key]) => key);
|
|
112
|
-
|
|
113
|
-
if (ids.length > 0) {
|
|
114
|
-
return ids as Extract<keyof M, string>[];
|
|
115
|
-
}
|
|
116
|
-
return ["id"] as Extract<keyof M, string>[];
|
|
117
|
-
}
|
package/src/util/common.ts
DELETED
package/src/util/conditions.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import jsonLogic from "json-logic-js";
|
|
2
|
-
import {
|
|
3
|
-
ArrayProperty,
|
|
4
|
-
AuthState,
|
|
5
|
-
ConditionContext,
|
|
6
|
-
ConditionRule,
|
|
7
|
-
EnumValueConfig,
|
|
8
|
-
NumberProperty,
|
|
9
|
-
PropertyConditions,
|
|
10
|
-
Property,
|
|
11
|
-
ReferenceProperty,
|
|
12
|
-
StringProperty
|
|
13
|
-
} from "@rebasepro/types";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Access a nested property from an object via dot notation.
|
|
17
|
-
*/
|
|
18
|
-
function getIn(obj: Record<string, unknown> | unknown, path: string): unknown {
|
|
19
|
-
if (!obj || !path) return undefined;
|
|
20
|
-
return path.split(".").reduce((acc: unknown, part: string) => acc && (acc as Record<string, unknown>)[part], obj);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let operationsRegistered = false;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Register custom JSON Logic operations for Rebase.
|
|
27
|
-
* Call this once at app initialization.
|
|
28
|
-
*/
|
|
29
|
-
export function registerConditionOperations(): void {
|
|
30
|
-
if (operationsRegistered) return;
|
|
31
|
-
|
|
32
|
-
// Check if user has a specific role by ID
|
|
33
|
-
jsonLogic.add_operation("hasRole", function (this: ConditionContext, roleId: string) {
|
|
34
|
-
return this?.user?.roles?.includes(roleId) ?? false;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Check if user has any of the specified roles
|
|
38
|
-
jsonLogic.add_operation("hasAnyRole", function (this: ConditionContext, roleIds: string[]) {
|
|
39
|
-
if (!this?.user?.roles || !Array.isArray(roleIds)) return false;
|
|
40
|
-
return roleIds.some(role => this.user.roles.includes(role));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Check if a timestamp is today
|
|
44
|
-
jsonLogic.add_operation("isToday", (timestamp: number) => {
|
|
45
|
-
if (!timestamp) return false;
|
|
46
|
-
const date = new Date(timestamp);
|
|
47
|
-
const today = new Date();
|
|
48
|
-
return date.getFullYear() === today.getFullYear() &&
|
|
49
|
-
date.getMonth() === today.getMonth() &&
|
|
50
|
-
date.getDate() === today.getDate();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Check if a timestamp is in the past
|
|
54
|
-
jsonLogic.add_operation("isPast", (timestamp: number) => {
|
|
55
|
-
if (!timestamp) return false;
|
|
56
|
-
return timestamp < Date.now();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Check if a timestamp is in the future
|
|
60
|
-
jsonLogic.add_operation("isFuture", (timestamp: number) => {
|
|
61
|
-
if (!timestamp) return false;
|
|
62
|
-
return timestamp > Date.now();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
operationsRegistered = true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Evaluate a condition against the given context.
|
|
70
|
-
*
|
|
71
|
-
* A condition may be stated as a literal instead of a rule — `hidden: true`
|
|
72
|
-
* rather than `hidden: { "==": [1, 1] }` — and a literal is already its own
|
|
73
|
-
* answer, so it is returned rather than handed to the evaluator.
|
|
74
|
-
*/
|
|
75
|
-
export function evaluateCondition(rule: ConditionRule, context: ConditionContext): unknown {
|
|
76
|
-
if (typeof rule === "boolean") return rule;
|
|
77
|
-
// Ensure operations are registered
|
|
78
|
-
registerConditionOperations();
|
|
79
|
-
return jsonLogic.apply(rule, context);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Convert a value to a format suitable for JSON Logic evaluation.
|
|
84
|
-
* Specifically handles Date objects by converting them to Unix timestamps.
|
|
85
|
-
*/
|
|
86
|
-
function serializeValueForConditions(value: unknown): unknown {
|
|
87
|
-
if (value === null || value === undefined) {
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Handle Date objects
|
|
92
|
-
if (value instanceof Date) {
|
|
93
|
-
return value.getTime();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Handle Firestore Timestamp-like objects (have toDate or toMillis)
|
|
97
|
-
if (typeof (value as { toMillis?: () => number })?.toMillis === "function") {
|
|
98
|
-
return (value as { toMillis: () => number }).toMillis();
|
|
99
|
-
}
|
|
100
|
-
if (typeof (value as { toDate?: () => Date })?.toDate === "function") {
|
|
101
|
-
return (value as { toDate: () => Date }).toDate().getTime();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Handle arrays recursively
|
|
105
|
-
if (Array.isArray(value)) {
|
|
106
|
-
return value.map(serializeValueForConditions);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Handle plain objects recursively
|
|
110
|
-
if (typeof value === "object") {
|
|
111
|
-
const result: Record<string, unknown> = {};
|
|
112
|
-
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
113
|
-
result[key] = serializeValueForConditions((value as Record<string, unknown>)[key]);
|
|
114
|
-
}
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return value;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Build a ConditionContext from the current property resolution context.
|
|
123
|
-
*/
|
|
124
|
-
export function buildConditionContext(params: {
|
|
125
|
-
propertyKey?: string;
|
|
126
|
-
values?: Record<string, unknown>;
|
|
127
|
-
previousValues?: Record<string, unknown>;
|
|
128
|
-
path: string;
|
|
129
|
-
entityId?: string;
|
|
130
|
-
index?: number;
|
|
131
|
-
authController: AuthState;
|
|
132
|
-
}): ConditionContext {
|
|
133
|
-
const {
|
|
134
|
-
propertyKey,
|
|
135
|
-
values,
|
|
136
|
-
previousValues,
|
|
137
|
-
path,
|
|
138
|
-
entityId,
|
|
139
|
-
index,
|
|
140
|
-
authController
|
|
141
|
-
} = params;
|
|
142
|
-
|
|
143
|
-
const user = authController.user;
|
|
144
|
-
const serializedValues = serializeValueForConditions(values ?? {});
|
|
145
|
-
const serializedPreviousValues = serializeValueForConditions(previousValues ?? values ?? {});
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
values: serializedValues as Record<string, unknown>,
|
|
149
|
-
previousValues: serializedPreviousValues as Record<string, unknown>,
|
|
150
|
-
propertyValue: propertyKey ? getIn(serializedValues, propertyKey) : undefined,
|
|
151
|
-
path,
|
|
152
|
-
entityId,
|
|
153
|
-
isNew: !entityId,
|
|
154
|
-
index,
|
|
155
|
-
user: {
|
|
156
|
-
uid: user?.uid ?? "",
|
|
157
|
-
email: user?.email ?? null,
|
|
158
|
-
displayName: user?.displayName ?? null,
|
|
159
|
-
photoURL: user?.photoURL ?? null,
|
|
160
|
-
roles: (user?.roles ?? []).map((r: unknown) => typeof r === "string" ? r : (r as { id: string }).id)
|
|
161
|
-
},
|
|
162
|
-
now: Date.now()
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
|
|
168
|
-
*/
|
package/src/util/email.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Email normalization — one implementation, because the database enforces it.
|
|
3
|
-
*
|
|
4
|
-
* `ensureAuthTablesExist` puts a `UNIQUE INDEX ON users (lower(email))` on the
|
|
5
|
-
* auth table. That index decides what "the same address" means, and it does not
|
|
6
|
-
* trim: to Postgres, `' foo@bar.com'` and `'foo@bar.com'` are two addresses and
|
|
7
|
-
* both may exist. So every write that reaches the column has to agree with
|
|
8
|
-
* every read, exactly, or the two disagree in the one direction that matters —
|
|
9
|
-
* a row that exists and cannot be found.
|
|
10
|
-
*
|
|
11
|
-
* That is not hypothetical. The lookup path trimmed and the admin create paths
|
|
12
|
-
* did not, so a user created through `POST /api/data/users` or
|
|
13
|
-
* `POST /api/auth/admin/users` with a stray space was stored untrimmed,
|
|
14
|
-
* survived the unique index alongside the real address, and was unreachable by
|
|
15
|
-
* login forever after. The HTTP auth routes were unaffected only because Zod's
|
|
16
|
-
* `.email()` happens to reject surrounding whitespace — a guard on a different
|
|
17
|
-
* layer, for a different reason, that the admin paths do not sit behind.
|
|
18
|
-
*
|
|
19
|
-
* It lives in `common` because `server`, `server-postgres` and `server-mongo`
|
|
20
|
-
* all write this column and must agree exactly, and `common` is the only
|
|
21
|
-
* package all three already depend on.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Canonical form of an email address: trimmed, lower-cased.
|
|
26
|
-
*
|
|
27
|
-
* Non-strings pass through untouched, so this is safe to apply to a value out
|
|
28
|
-
* of a partial update payload whose type is not known yet.
|
|
29
|
-
*/
|
|
30
|
-
export function normalizeEmail<T>(email: T): T | string {
|
|
31
|
-
return typeof email === "string" ? email.trim().toLowerCase() : email;
|
|
32
|
-
}
|