@rebasepro/common 0.0.1-canary.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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/collections/CollectionRegistry.d.ts +48 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +2380 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2379 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/util/arrays.d.ts +1 -0
- package/dist/util/builders.d.ts +64 -0
- package/dist/util/callbacks.d.ts +6 -0
- package/dist/util/collections.d.ts +11 -0
- package/dist/util/common.d.ts +2 -0
- package/dist/util/conditions.d.ts +26 -0
- package/dist/util/dates.d.ts +1 -0
- package/dist/util/entities.d.ts +28 -0
- package/dist/util/entity_actions.d.ts +2 -0
- package/dist/util/enums.d.ts +3 -0
- package/dist/util/fields.d.ts +2 -0
- package/dist/util/flatten_object.d.ts +5 -0
- package/dist/util/hash.d.ts +1 -0
- package/dist/util/index.d.ts +26 -0
- package/dist/util/names.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +29 -0
- package/dist/util/navigation_utils.d.ts +31 -0
- package/dist/util/objects.d.ts +26 -0
- package/dist/util/os.d.ts +2 -0
- package/dist/util/parent_references_from_path.d.ts +6 -0
- package/dist/util/paths.d.ts +14 -0
- package/dist/util/permissions.d.ts +5 -0
- package/dist/util/permissions.test.d.ts +1 -0
- package/dist/util/plurals.d.ts +16 -0
- package/dist/util/references.d.ts +2 -0
- package/dist/util/regexp.d.ts +7 -0
- package/dist/util/relations.d.ts +12 -0
- package/dist/util/resolutions.d.ts +74 -0
- package/dist/util/storage.d.ts +24 -0
- package/dist/util/strings.d.ts +7 -0
- package/package.json +118 -0
- package/src/collections/CollectionRegistry.ts +319 -0
- package/src/collections/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/util/arrays.ts +3 -0
- package/src/util/builders.ts +138 -0
- package/src/util/callbacks.ts +115 -0
- package/src/util/collections.ts +126 -0
- package/src/util/common.ts +2 -0
- package/src/util/conditions.ts +348 -0
- package/src/util/dates.ts +1 -0
- package/src/util/entities.ts +212 -0
- package/src/util/entity_actions.ts +28 -0
- package/src/util/enums.ts +26 -0
- package/src/util/fields.ts +28 -0
- package/src/util/flatten_object.ts +45 -0
- package/src/util/hash.ts +11 -0
- package/src/util/index.ts +26 -0
- package/src/util/names.ts +30 -0
- package/src/util/navigation_from_path.ts +121 -0
- package/src/util/navigation_utils.ts +222 -0
- package/src/util/objects.ts +376 -0
- package/src/util/os.ts +13 -0
- package/src/util/parent_references_from_path.ts +57 -0
- package/src/util/paths.ts +27 -0
- package/src/util/permissions.test.ts +716 -0
- package/src/util/permissions.ts +235 -0
- package/src/util/plurals.ts +188 -0
- package/src/util/references.ts +34 -0
- package/src/util/regexp.ts +32 -0
- package/src/util/relations.ts +211 -0
- package/src/util/resolutions.ts +383 -0
- package/src/util/storage.ts +144 -0
- package/src/util/strings.ts +84 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { AuthController, Entity, EntityCollection, SecurityRule, User } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
function evaluateAST(sqlString: string, auth: AuthController, entity: Entity<any> | null): boolean {
|
|
4
|
+
// This is a client-side SQL evaluator used *only* for optimistic UI updates.
|
|
5
|
+
// It parses basic AND / OR statements to evaluate RLS without backend roundtrips.
|
|
6
|
+
if (!entity) return true;
|
|
7
|
+
|
|
8
|
+
// 1. Clean outer parentheses
|
|
9
|
+
let cleanedSQL = sqlString.trim();
|
|
10
|
+
while (cleanedSQL.startsWith('(') && cleanedSQL.endsWith(')')) {
|
|
11
|
+
let openCount = 0;
|
|
12
|
+
let isEnclosing = true;
|
|
13
|
+
for (let i = 0; i < cleanedSQL.length - 1; i++) {
|
|
14
|
+
if (cleanedSQL[i] === '(') openCount++;
|
|
15
|
+
else if (cleanedSQL[i] === ')') openCount--;
|
|
16
|
+
if (openCount === 0) {
|
|
17
|
+
isEnclosing = false;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (isEnclosing) {
|
|
22
|
+
cleanedSQL = cleanedSQL.substring(1, cleanedSQL.length - 1).trim();
|
|
23
|
+
} else {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Split top-level OR / AND
|
|
29
|
+
const splitByTopLevel = (str: string, delimiter: string) => {
|
|
30
|
+
const parts: string[] = [];
|
|
31
|
+
let current = "";
|
|
32
|
+
let openCount = 0;
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < str.length) {
|
|
35
|
+
if (str[i] === '(') openCount++;
|
|
36
|
+
else if (str[i] === ')') openCount--;
|
|
37
|
+
|
|
38
|
+
if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
|
|
39
|
+
parts.push(current);
|
|
40
|
+
current = "";
|
|
41
|
+
i += delimiter.length;
|
|
42
|
+
} else {
|
|
43
|
+
current += str[i];
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
parts.push(current);
|
|
48
|
+
return parts;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const orParts = splitByTopLevel(cleanedSQL, " OR ");
|
|
52
|
+
if (orParts.length > 1) {
|
|
53
|
+
return orParts.some(part => evaluateAST(part, auth, entity));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const andParts = splitByTopLevel(cleanedSQL, " AND ");
|
|
57
|
+
if (andParts.length > 1) {
|
|
58
|
+
return andParts.every(part => evaluateAST(part, auth, entity));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const upperSQL = cleanedSQL.toUpperCase();
|
|
62
|
+
|
|
63
|
+
// 3. Fallback for unparseable complex queries
|
|
64
|
+
if (upperSQL.includes(" IN ") || upperSQL.includes(" EXISTS ")) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Role array checks
|
|
69
|
+
// Pattern: `string_to_array(auth.roles(), ',') && ARRAY['admin', 'editor']`
|
|
70
|
+
const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
|
|
71
|
+
if (roleIntersectMatch && roleIntersectMatch[1]) {
|
|
72
|
+
const requiredRoles = roleIntersectMatch[1].split(',').map(r => r.trim().replace(/'/g, ''));
|
|
73
|
+
const userRoles = auth.user?.roles || [];
|
|
74
|
+
return requiredRoles.some(r => userRoles.includes(r));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Pattern: `string_to_array(auth.roles(), ',') @> ARRAY['admin']`
|
|
78
|
+
const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
|
|
79
|
+
if (roleContainMatch && roleContainMatch[1]) {
|
|
80
|
+
const requiredRoles = roleContainMatch[1].split(',').map(r => r.trim().replace(/'/g, ''));
|
|
81
|
+
const userRoles = auth.user?.roles || [];
|
|
82
|
+
return requiredRoles.every(r => userRoles.includes(r));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 5. Existing ID patterns
|
|
86
|
+
const pattern1 = new RegExp(`^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))`);
|
|
87
|
+
const pattern2 = new RegExp(`^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?`);
|
|
88
|
+
|
|
89
|
+
const match1 = cleanedSQL.match(pattern1);
|
|
90
|
+
if (match1 && match1[1]) {
|
|
91
|
+
return entity.values[match1[1]] === auth.user?.uid;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const match2 = cleanedSQL.match(pattern2);
|
|
95
|
+
if (match2 && match2[1]) {
|
|
96
|
+
return entity.values[match2[1]] === auth.user?.uid;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. Simple equality
|
|
100
|
+
// Pattern: `field = 'value'` or `{field} != 'value'`
|
|
101
|
+
const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
|
|
102
|
+
if (simpleEqualityMatch) {
|
|
103
|
+
const field = simpleEqualityMatch[1];
|
|
104
|
+
const operator = simpleEqualityMatch[2];
|
|
105
|
+
const value = simpleEqualityMatch[3];
|
|
106
|
+
const entityValue = entity.values[field];
|
|
107
|
+
if (operator === "=") return entityValue === value;
|
|
108
|
+
if (operator === "!=") return entityValue !== value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true; // Optimistic fallback for anything else
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function evaluateRule(rule: SecurityRule, auth: AuthController, entity: Entity<any> | null): boolean {
|
|
115
|
+
|
|
116
|
+
if (rule.access === "public") return true;
|
|
117
|
+
|
|
118
|
+
if (rule.ownerField) {
|
|
119
|
+
if (!entity) {
|
|
120
|
+
// null entity: optimistic — we can't evaluate ownership without data
|
|
121
|
+
// Fall through to SQL checks below (if any). If none, will return true.
|
|
122
|
+
} else {
|
|
123
|
+
// Entity present: strictly check ownership. Fail immediately if mismatch.
|
|
124
|
+
if (entity.values[rule.ownerField] !== auth.user?.uid) return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// In PostgreSQL RLS, USING and WITH CHECK have distinct semantics:
|
|
129
|
+
// USING applies to existing rows (SELECT/UPDATE/DELETE read phase)
|
|
130
|
+
// WITH CHECK applies to new/modified values (INSERT/UPDATE write phase)
|
|
131
|
+
// Both must pass. We evaluate both independently.
|
|
132
|
+
if (rule.using && !evaluateAST(rule.using, auth, entity)) return false;
|
|
133
|
+
if (rule.withCheck && !evaluateAST(rule.withCheck, auth, entity)) return false;
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function checkOperation(
|
|
139
|
+
collection: EntityCollection<any>,
|
|
140
|
+
authController: AuthController<any>,
|
|
141
|
+
entity: Entity<any> | null,
|
|
142
|
+
targetOperation: "select" | "insert" | "update" | "delete"
|
|
143
|
+
): boolean {
|
|
144
|
+
if (!collection.securityRules || collection.securityRules.length === 0) {
|
|
145
|
+
// According to our plan: Postgres RLS implicitly denies if enabled without rules.
|
|
146
|
+
// But for Rebase we default to true if securityRules is undefined,
|
|
147
|
+
// so as not to break everything without rules. Let's assume true for now.
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const applicableRules = collection.securityRules.filter(r =>
|
|
152
|
+
r.operation === targetOperation ||
|
|
153
|
+
r.operation === "all" ||
|
|
154
|
+
r.operations?.includes(targetOperation) ||
|
|
155
|
+
r.operations?.includes("all")
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (applicableRules.length === 0) return false;
|
|
159
|
+
|
|
160
|
+
// In Postgres, policies ONLY apply if the user matching the targeted roles.
|
|
161
|
+
const userRoleIds = authController.user?.roles ?? [];
|
|
162
|
+
const userRoles = [...userRoleIds, "public"];
|
|
163
|
+
const roleApplicableRules = applicableRules.filter(rule => {
|
|
164
|
+
if (!rule.roles || rule.roles.length === 0) return true; // APPLIES TO PUBLIC
|
|
165
|
+
return rule.roles.some((r: string) => userRoles.includes(r));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// If no rules apply to this user's roles, the operation is implicitly denied.
|
|
169
|
+
if (roleApplicableRules.length === 0) return false;
|
|
170
|
+
|
|
171
|
+
let grantedByPermissive = false;
|
|
172
|
+
let deniedByRestrictive = false;
|
|
173
|
+
|
|
174
|
+
for (const rule of roleApplicableRules) {
|
|
175
|
+
const mode = rule.mode || "permissive";
|
|
176
|
+
const passed = evaluateRule(rule, authController, entity);
|
|
177
|
+
|
|
178
|
+
if (mode === "restrictive" && !passed) {
|
|
179
|
+
deniedByRestrictive = true;
|
|
180
|
+
break; // Immediate deny
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (mode === "permissive" && passed) {
|
|
184
|
+
grantedByPermissive = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (deniedByRestrictive) return false;
|
|
189
|
+
|
|
190
|
+
const hasPermissive = roleApplicableRules.some(r => (r.mode || "permissive") === "permissive");
|
|
191
|
+
if (hasPermissive) {
|
|
192
|
+
return grantedByPermissive;
|
|
193
|
+
} else {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function canReadCollection<M extends Record<string, any>, USER extends User>
|
|
199
|
+
(
|
|
200
|
+
collection: EntityCollection<M>,
|
|
201
|
+
authController: AuthController<USER>
|
|
202
|
+
): boolean {
|
|
203
|
+
return checkOperation(collection, authController, null, "select");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function canEditEntity<M extends Record<string, any>, USER extends User>
|
|
207
|
+
(
|
|
208
|
+
collection: EntityCollection<M>,
|
|
209
|
+
authController: AuthController<USER>,
|
|
210
|
+
path: string,
|
|
211
|
+
entity: Entity<M> | null
|
|
212
|
+
): boolean {
|
|
213
|
+
return checkOperation(collection, authController, entity, "update");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function canCreateEntity<M extends Record<string, any>, USER extends User>
|
|
217
|
+
(
|
|
218
|
+
collection: EntityCollection<M>,
|
|
219
|
+
authController: AuthController<USER>,
|
|
220
|
+
path: string,
|
|
221
|
+
entity: Entity<M> | null
|
|
222
|
+
): boolean {
|
|
223
|
+
if (collection.collectionGroup) return false;
|
|
224
|
+
return checkOperation(collection, authController, entity, "insert");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function canDeleteEntity<M extends Record<string, any>, USER extends User>
|
|
228
|
+
(
|
|
229
|
+
collection: EntityCollection<M>,
|
|
230
|
+
authController: AuthController<USER>,
|
|
231
|
+
path: string,
|
|
232
|
+
entity: Entity<M> | null
|
|
233
|
+
): boolean {
|
|
234
|
+
return checkOperation(collection, authController, entity, "delete");
|
|
235
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the plural of an English word.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} word
|
|
5
|
+
* @param {number} [amount]
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function plural(word: string, amount?: number): string {
|
|
9
|
+
if (amount !== undefined && amount === 1) {
|
|
10
|
+
return word
|
|
11
|
+
}
|
|
12
|
+
const plurals: { [key: string]: string } = {
|
|
13
|
+
"(quiz)$": "$1zes",
|
|
14
|
+
"^(ox)$": "$1en",
|
|
15
|
+
"([m|l])ouse$": "$1ice",
|
|
16
|
+
"(matr|vert|ind)ix|ex$": "$1ices",
|
|
17
|
+
"(x|ch|ss|sh)$": "$1es",
|
|
18
|
+
"([^aeiouy]|qu)y$": "$1ies",
|
|
19
|
+
"(hive)$": "$1s",
|
|
20
|
+
"(?:([^f])fe|([lr])f)$": "$1$2ves",
|
|
21
|
+
"(shea|lea|loa|thie)f$": "$1ves",
|
|
22
|
+
sis$: "ses",
|
|
23
|
+
"([ti])um$": "$1a",
|
|
24
|
+
"(tomat|potat|ech|her|vet)o$": "$1oes",
|
|
25
|
+
"(bu)s$": "$1ses",
|
|
26
|
+
"(alias)$": "$1es",
|
|
27
|
+
"(octop)us$": "$1i",
|
|
28
|
+
"(ax|test)is$": "$1es",
|
|
29
|
+
"(us)$": "$1es",
|
|
30
|
+
"([^s]+)$": "$1s"
|
|
31
|
+
}
|
|
32
|
+
const irregular: { [key: string]: string } = {
|
|
33
|
+
move: "moves",
|
|
34
|
+
foot: "feet",
|
|
35
|
+
goose: "geese",
|
|
36
|
+
sex: "sexes",
|
|
37
|
+
child: "children",
|
|
38
|
+
man: "men",
|
|
39
|
+
tooth: "teeth",
|
|
40
|
+
person: "people"
|
|
41
|
+
}
|
|
42
|
+
const uncountable: string[] = [
|
|
43
|
+
"sheep",
|
|
44
|
+
"fish",
|
|
45
|
+
"deer",
|
|
46
|
+
"moose",
|
|
47
|
+
"series",
|
|
48
|
+
"species",
|
|
49
|
+
"money",
|
|
50
|
+
"rice",
|
|
51
|
+
"information",
|
|
52
|
+
"equipment",
|
|
53
|
+
"bison",
|
|
54
|
+
"cod",
|
|
55
|
+
"offspring",
|
|
56
|
+
"pike",
|
|
57
|
+
"salmon",
|
|
58
|
+
"shrimp",
|
|
59
|
+
"swine",
|
|
60
|
+
"trout",
|
|
61
|
+
"aircraft",
|
|
62
|
+
"hovercraft",
|
|
63
|
+
"spacecraft",
|
|
64
|
+
"sugar",
|
|
65
|
+
"tuna",
|
|
66
|
+
"you",
|
|
67
|
+
"wood"
|
|
68
|
+
]
|
|
69
|
+
// save some time in the case that singular and plural are the same
|
|
70
|
+
if (uncountable.indexOf(word.toLowerCase()) >= 0) {
|
|
71
|
+
return word;
|
|
72
|
+
}
|
|
73
|
+
// check for irregular forms
|
|
74
|
+
for (const w in irregular) {
|
|
75
|
+
const pattern = new RegExp(`${w}$`, "i")
|
|
76
|
+
const replace = irregular[w]
|
|
77
|
+
if (pattern.test(word)) {
|
|
78
|
+
return word.replace(pattern, replace);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// check for matches using regular expressions
|
|
82
|
+
for (const reg in plurals) {
|
|
83
|
+
const pattern = new RegExp(reg, "i")
|
|
84
|
+
if (pattern.test(word)) {
|
|
85
|
+
return word.replace(pattern, plurals[reg])
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return word;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the singular of an English word.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} word
|
|
95
|
+
* @param {number} [amount]
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function singular(word: string, amount?: number): string {
|
|
99
|
+
if (amount !== undefined && amount !== 1) {
|
|
100
|
+
return word;
|
|
101
|
+
}
|
|
102
|
+
const singulars: { [key: string]: string } = {
|
|
103
|
+
"(quiz)zes$": "$1",
|
|
104
|
+
"(matr)ices$": "$1ix",
|
|
105
|
+
"(vert|ind)ices$": "$1ex",
|
|
106
|
+
"^(ox)en$": "$1",
|
|
107
|
+
"(alias)es$": "$1",
|
|
108
|
+
"(octop|vir)i$": "$1us",
|
|
109
|
+
"(cris|ax|test)es$": "$1is",
|
|
110
|
+
"(shoe)s$": "$1",
|
|
111
|
+
"(o)es$": "$1",
|
|
112
|
+
"(bus)es$": "$1",
|
|
113
|
+
"([m|l])ice$": "$1ouse",
|
|
114
|
+
"(x|ch|ss|sh)es$": "$1",
|
|
115
|
+
"(m)ovies$": "$1ovie",
|
|
116
|
+
"(s)eries$": "$1eries",
|
|
117
|
+
"([^aeiouy]|qu)ies$": "$1y",
|
|
118
|
+
"([lr])ves$": "$1f",
|
|
119
|
+
"(tive)s$": "$1",
|
|
120
|
+
"(hive)s$": "$1",
|
|
121
|
+
"(li|wi|kni)ves$": "$1fe",
|
|
122
|
+
"(shea|loa|lea|thie)ves$": "$1f",
|
|
123
|
+
"(^analy)ses$": "$1sis",
|
|
124
|
+
"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$": "$1$2sis",
|
|
125
|
+
"([ti])a$": "$1um",
|
|
126
|
+
"(n)ews$": "$1ews",
|
|
127
|
+
"(h|bl)ouses$": "$1ouse",
|
|
128
|
+
"(corpse)s$": "$1",
|
|
129
|
+
"(us)es$": "$1",
|
|
130
|
+
s$: ""
|
|
131
|
+
}
|
|
132
|
+
const irregular: { [key: string]: string } = {
|
|
133
|
+
move: "moves",
|
|
134
|
+
foot: "feet",
|
|
135
|
+
goose: "geese",
|
|
136
|
+
sex: "sexes",
|
|
137
|
+
child: "children",
|
|
138
|
+
man: "men",
|
|
139
|
+
tooth: "teeth",
|
|
140
|
+
person: "people"
|
|
141
|
+
}
|
|
142
|
+
const uncountable: string[] = [
|
|
143
|
+
"sheep",
|
|
144
|
+
"fish",
|
|
145
|
+
"deer",
|
|
146
|
+
"moose",
|
|
147
|
+
"series",
|
|
148
|
+
"species",
|
|
149
|
+
"money",
|
|
150
|
+
"rice",
|
|
151
|
+
"information",
|
|
152
|
+
"equipment",
|
|
153
|
+
"bison",
|
|
154
|
+
"cod",
|
|
155
|
+
"offspring",
|
|
156
|
+
"pike",
|
|
157
|
+
"salmon",
|
|
158
|
+
"shrimp",
|
|
159
|
+
"swine",
|
|
160
|
+
"trout",
|
|
161
|
+
"aircraft",
|
|
162
|
+
"hovercraft",
|
|
163
|
+
"spacecraft",
|
|
164
|
+
"sugar",
|
|
165
|
+
"tuna",
|
|
166
|
+
"you",
|
|
167
|
+
"wood"
|
|
168
|
+
]
|
|
169
|
+
// save some time in the case that singular and plural are the same
|
|
170
|
+
if (uncountable.indexOf(word.toLowerCase()) >= 0) {
|
|
171
|
+
return word;
|
|
172
|
+
}
|
|
173
|
+
// check for irregular forms
|
|
174
|
+
for (const w in irregular) {
|
|
175
|
+
const pattern = new RegExp(`${irregular[w]}$`, "i");
|
|
176
|
+
if (pattern.test(word)) {
|
|
177
|
+
return word.replace(pattern, w);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// check for matches using regular expressions
|
|
181
|
+
for (const reg in singulars) {
|
|
182
|
+
const pattern = new RegExp(reg, "i");
|
|
183
|
+
if (pattern.test(word)) {
|
|
184
|
+
return word.replace(pattern, singulars[reg]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return word;
|
|
188
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
export function getEntityImagePreviewPropertyKey<M extends object>(collection: EntityCollection<M>): string | undefined {
|
|
4
|
+
|
|
5
|
+
// find first storage property of type image
|
|
6
|
+
for (const key in collection.properties) {
|
|
7
|
+
const property = collection.properties[key];
|
|
8
|
+
if (property.type === "string" && property.storage?.acceptedFiles?.includes("image/*")) {
|
|
9
|
+
return key;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// alternatively, look for the first array of images
|
|
13
|
+
for (const key in collection.properties) {
|
|
14
|
+
const property = collection.properties[key];
|
|
15
|
+
if (property.type === "array" && !Array.isArray(property.of) && property.of?.type === "string" && property.of.storage?.acceptedFiles?.includes("image/*")) {
|
|
16
|
+
return key;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// also check for URL properties with image preview type
|
|
20
|
+
for (const key in collection.properties) {
|
|
21
|
+
const property = collection.properties[key];
|
|
22
|
+
if (property.type === "string" && property.url === "image") {
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// and arrays of URL properties with image preview type
|
|
27
|
+
for (const key in collection.properties) {
|
|
28
|
+
const property = collection.properties[key];
|
|
29
|
+
if (property.type === "array" && property.of?.type === "string" && property.of.url === "image") {
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function serializeRegExp(input: RegExp): string {
|
|
2
|
+
if (!input) return "";
|
|
3
|
+
// const fragments = input.toString().match(/\/(.*?)\/([a-z]*)?$/i);
|
|
4
|
+
// if (fragments) {
|
|
5
|
+
// if (fragments[2])
|
|
6
|
+
// return input.toString();
|
|
7
|
+
// return fragments[1];
|
|
8
|
+
// }
|
|
9
|
+
return input.toString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get a RegExp out of a serialized string
|
|
14
|
+
* @param input
|
|
15
|
+
*/
|
|
16
|
+
export function hydrateRegExp(input?: string): RegExp | undefined {
|
|
17
|
+
if (!input) return undefined;
|
|
18
|
+
const fragments = input.match(/\/(.*?)\/([a-z]*)?$/i);
|
|
19
|
+
if (fragments) {
|
|
20
|
+
return new RegExp(fragments[1], fragments[2] || "");
|
|
21
|
+
} else {
|
|
22
|
+
return new RegExp(input, "");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isValidRegExp(input: string): boolean {
|
|
27
|
+
const fullRegexp = input.match(/\/((?![*+?])(?:[^\r\n[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*])+)\/((?:g(?:im?|mi?)?|i(?:gm?|mg?)?|m(?:gi?|ig?)?)?)/);
|
|
28
|
+
if (fullRegexp)
|
|
29
|
+
return true;
|
|
30
|
+
const simpleRegexp = input.match(/((?![*+?])(?:[^\r\n[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*])+)/);
|
|
31
|
+
return !!simpleRegexp;
|
|
32
|
+
}
|