@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/collections/CollectionRegistry.d.ts +48 -0
  4. package/dist/collections/index.d.ts +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.es.js +2380 -0
  7. package/dist/index.es.js.map +1 -0
  8. package/dist/index.umd.js +2379 -0
  9. package/dist/index.umd.js.map +1 -0
  10. package/dist/util/arrays.d.ts +1 -0
  11. package/dist/util/builders.d.ts +64 -0
  12. package/dist/util/callbacks.d.ts +6 -0
  13. package/dist/util/collections.d.ts +11 -0
  14. package/dist/util/common.d.ts +2 -0
  15. package/dist/util/conditions.d.ts +26 -0
  16. package/dist/util/dates.d.ts +1 -0
  17. package/dist/util/entities.d.ts +28 -0
  18. package/dist/util/entity_actions.d.ts +2 -0
  19. package/dist/util/enums.d.ts +3 -0
  20. package/dist/util/fields.d.ts +2 -0
  21. package/dist/util/flatten_object.d.ts +5 -0
  22. package/dist/util/hash.d.ts +1 -0
  23. package/dist/util/index.d.ts +26 -0
  24. package/dist/util/names.d.ts +22 -0
  25. package/dist/util/navigation_from_path.d.ts +29 -0
  26. package/dist/util/navigation_utils.d.ts +31 -0
  27. package/dist/util/objects.d.ts +26 -0
  28. package/dist/util/os.d.ts +2 -0
  29. package/dist/util/parent_references_from_path.d.ts +6 -0
  30. package/dist/util/paths.d.ts +14 -0
  31. package/dist/util/permissions.d.ts +5 -0
  32. package/dist/util/permissions.test.d.ts +1 -0
  33. package/dist/util/plurals.d.ts +16 -0
  34. package/dist/util/references.d.ts +2 -0
  35. package/dist/util/regexp.d.ts +7 -0
  36. package/dist/util/relations.d.ts +12 -0
  37. package/dist/util/resolutions.d.ts +74 -0
  38. package/dist/util/storage.d.ts +24 -0
  39. package/dist/util/strings.d.ts +7 -0
  40. package/package.json +118 -0
  41. package/src/collections/CollectionRegistry.ts +319 -0
  42. package/src/collections/index.ts +1 -0
  43. package/src/index.ts +2 -0
  44. package/src/util/arrays.ts +3 -0
  45. package/src/util/builders.ts +138 -0
  46. package/src/util/callbacks.ts +115 -0
  47. package/src/util/collections.ts +126 -0
  48. package/src/util/common.ts +2 -0
  49. package/src/util/conditions.ts +348 -0
  50. package/src/util/dates.ts +1 -0
  51. package/src/util/entities.ts +212 -0
  52. package/src/util/entity_actions.ts +28 -0
  53. package/src/util/enums.ts +26 -0
  54. package/src/util/fields.ts +28 -0
  55. package/src/util/flatten_object.ts +45 -0
  56. package/src/util/hash.ts +11 -0
  57. package/src/util/index.ts +26 -0
  58. package/src/util/names.ts +30 -0
  59. package/src/util/navigation_from_path.ts +121 -0
  60. package/src/util/navigation_utils.ts +222 -0
  61. package/src/util/objects.ts +376 -0
  62. package/src/util/os.ts +13 -0
  63. package/src/util/parent_references_from_path.ts +57 -0
  64. package/src/util/paths.ts +27 -0
  65. package/src/util/permissions.test.ts +716 -0
  66. package/src/util/permissions.ts +235 -0
  67. package/src/util/plurals.ts +188 -0
  68. package/src/util/references.ts +34 -0
  69. package/src/util/regexp.ts +32 -0
  70. package/src/util/relations.ts +211 -0
  71. package/src/util/resolutions.ts +383 -0
  72. package/src/util/storage.ts +144 -0
  73. 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
+ }