@stackframe/stack-shared 2.8.3 → 2.8.6
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/CHANGELOG.md +18 -0
- package/dist/config/format.d.ts +38 -0
- package/dist/config/format.js +224 -0
- package/dist/config/schema.d.ts +721 -0
- package/dist/config/schema.js +185 -0
- package/dist/crud.js +1 -1
- package/dist/interface/adminInterface.js +3 -3
- package/dist/interface/clientInterface.d.ts +4 -4
- package/dist/interface/crud/current-user.d.ts +1 -1
- package/dist/interface/crud/project-api-keys.d.ts +4 -4
- package/dist/interface/crud/projects.d.ts +25 -20
- package/dist/interface/crud/projects.js +9 -5
- package/dist/interface/crud/team-member-profiles.d.ts +2 -2
- package/dist/interface/crud/users.d.ts +4 -4
- package/dist/known-errors.d.ts +14 -0
- package/dist/known-errors.js +20 -0
- package/dist/schema-fields.d.ts +5 -4
- package/dist/schema-fields.js +62 -3
- package/dist/utils/api-keys.js +11 -10
- package/dist/utils/caches.d.ts +10 -10
- package/dist/utils/objects.d.ts +28 -3
- package/dist/utils/objects.js +117 -0
- package/dist/utils/promises.d.ts +1 -1
- package/dist/utils/promises.js +9 -10
- package/dist/utils/stores.d.ts +6 -6
- package/dist/utils/types.d.ts +2 -2
- package/package.json +1 -1
package/dist/schema-fields.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ declare module "yup" {
|
|
|
5
5
|
empty(): StringSchema<TType, TContext, TDefault, TFlags>;
|
|
6
6
|
}
|
|
7
7
|
interface Schema<TType, TContext, TDefault, TFlags> {
|
|
8
|
-
getNested<K extends keyof TType
|
|
9
|
-
concat<U extends yup.AnySchema>(schema: U): yup.Schema<Omit<TType
|
|
8
|
+
getNested<K extends keyof NonNullable<TType>>(path: K): yup.Schema<NonNullable<TType>[K], TContext, TDefault, TFlags>;
|
|
9
|
+
concat<U extends yup.AnySchema>(schema: U): yup.Schema<Omit<NonNullable<TType>, keyof yup.InferType<U>> & yup.InferType<U> | (TType & (null | undefined)), TContext, TDefault, TFlags>;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
export declare function yupValidate<S extends yup.ISchema<any>>(schema: S, obj: unknown, options?: yup.ValidateOptions & {
|
|
@@ -27,6 +27,7 @@ export declare function yupTuple<T extends [unknown, ...unknown[]]>(...args: Par
|
|
|
27
27
|
export declare function yupObject<A extends yup.Maybe<yup.AnyObject>, B extends yup.ObjectShape>(...args: Parameters<typeof yup.object<A, B>>): yup.ObjectSchema<yup.TypeFromShape<B, yup.AnyObject> extends infer T ? T extends yup.TypeFromShape<B, yup.AnyObject> ? T extends {} ? { [k in keyof T]: T[k]; } : T : never : never, yup.AnyObject, yup.DefaultFromShape<B> extends infer T_1 ? T_1 extends yup.DefaultFromShape<B> ? T_1 extends {} ? { [k_1 in keyof T_1]: T_1[k_1]; } : T_1 : never : never, "">;
|
|
28
28
|
export declare function yupNever(): yup.MixedSchema<never>;
|
|
29
29
|
export declare function yupUnion<T extends yup.ISchema<any>[]>(...args: T): yup.MixedSchema<yup.InferType<T[number]>>;
|
|
30
|
+
export declare function yupRecord<K extends yup.StringSchema, T extends yup.AnySchema>(keySchema: K, valueSchema: T): yup.MixedSchema<Record<string, yup.InferType<T>>>;
|
|
30
31
|
export declare function ensureObjectSchema<T extends yup.AnyObject>(schema: yup.Schema<T>): yup.ObjectSchema<T> & typeof schema;
|
|
31
32
|
export declare const adaptSchema: yup.MixedSchema<typeof StackAdaptSentinel | undefined, yup.AnyObject, undefined, "">;
|
|
32
33
|
/**
|
|
@@ -65,7 +66,7 @@ export declare const projectClientTeamCreationEnabledSchema: yup.BooleanSchema<b
|
|
|
65
66
|
export declare const projectClientUserDeletionEnabledSchema: yup.BooleanSchema<boolean | undefined, yup.AnyObject, undefined, "">;
|
|
66
67
|
export declare const projectSignUpEnabledSchema: yup.BooleanSchema<boolean | undefined, yup.AnyObject, undefined, "">;
|
|
67
68
|
export declare const projectCredentialEnabledSchema: yup.BooleanSchema<boolean | undefined, yup.AnyObject, undefined, "">;
|
|
68
|
-
export declare const oauthIdSchema: yup.StringSchema<"
|
|
69
|
+
export declare const oauthIdSchema: yup.StringSchema<"apple" | "x" | "google" | "github" | "microsoft" | "spotify" | "facebook" | "discord" | "gitlab" | "bitbucket" | "linkedin" | undefined, yup.AnyObject, undefined, "">;
|
|
69
70
|
export declare const oauthEnabledSchema: yup.BooleanSchema<boolean | undefined, yup.AnyObject, undefined, "">;
|
|
70
71
|
export declare const oauthTypeSchema: yup.StringSchema<"shared" | "standard" | undefined, yup.AnyObject, undefined, "">;
|
|
71
72
|
export declare const oauthClientIdSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
@@ -99,7 +100,7 @@ export declare const userClientReadOnlyMetadataSchema: yup.MixedSchema<{} | null
|
|
|
99
100
|
export declare const userServerMetadataSchema: yup.MixedSchema<{} | null, yup.AnyObject, undefined, "">;
|
|
100
101
|
export declare const userOAuthProviderSchema: yup.ObjectSchema<{
|
|
101
102
|
id: string;
|
|
102
|
-
type: "
|
|
103
|
+
type: "apple" | "x" | "google" | "github" | "microsoft" | "spotify" | "facebook" | "discord" | "gitlab" | "bitbucket" | "linkedin";
|
|
103
104
|
provider_user_id: string;
|
|
104
105
|
}, yup.AnyObject, {
|
|
105
106
|
id: undefined;
|
package/dist/schema-fields.js
CHANGED
|
@@ -118,9 +118,9 @@ export function yupObject(...args) {
|
|
|
118
118
|
if (unknownKeys.length > 0) {
|
|
119
119
|
// TODO "did you mean XYZ"
|
|
120
120
|
return context.createError({
|
|
121
|
-
message: `${context.path} contains unknown properties: ${unknownKeys.join(', ')}`,
|
|
121
|
+
message: `${context.path || "Object"} contains unknown properties: ${unknownKeys.join(', ')}`,
|
|
122
122
|
path: context.path,
|
|
123
|
-
params: { unknownKeys },
|
|
123
|
+
params: { unknownKeys, availableKeys },
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -160,6 +160,38 @@ export function yupUnion(...args) {
|
|
|
160
160
|
});
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
|
+
export function yupRecord(keySchema, valueSchema) {
|
|
164
|
+
return yupObject().unknown(true).test('record', '${path} must be a record of valid values', async function (value, context) {
|
|
165
|
+
if (value == null)
|
|
166
|
+
return true;
|
|
167
|
+
const { path, createError } = this;
|
|
168
|
+
if (typeof value !== 'object') {
|
|
169
|
+
return createError({ message: `${path} must be an object` });
|
|
170
|
+
}
|
|
171
|
+
// Validate each property using the provided valueSchema
|
|
172
|
+
for (const key of Object.keys(value)) {
|
|
173
|
+
// Validate the key
|
|
174
|
+
await yupValidate(keySchema, key, context.options);
|
|
175
|
+
// Validate the value
|
|
176
|
+
try {
|
|
177
|
+
await yupValidate(valueSchema, value[key], {
|
|
178
|
+
...context.options,
|
|
179
|
+
context: {
|
|
180
|
+
...context.options.context,
|
|
181
|
+
path: path ? `${path}.${key}` : key,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
return createError({
|
|
187
|
+
path: path ? `${path}.${key}` : key,
|
|
188
|
+
message: e.message,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
163
195
|
export function ensureObjectSchema(schema) {
|
|
164
196
|
if (!(schema instanceof yup.ObjectSchema))
|
|
165
197
|
throw new StackAssertionError(`assertObjectSchema: schema is not an ObjectSchema: ${schema.describe().type}`);
|
|
@@ -216,9 +248,36 @@ export const passwordSchema = yupString().max(70);
|
|
|
216
248
|
* `emailSchema` instead until we do the DB migration.
|
|
217
249
|
*/
|
|
218
250
|
// eslint-disable-next-line no-restricted-syntax
|
|
219
|
-
export const strictEmailSchema = (message) => yupString().email(message).matches(/^[^.]
|
|
251
|
+
export const strictEmailSchema = (message) => yupString().email(message).matches(/^[^.]+(\.[^.]+)*@.*\.[^.][^.]+$/, message);
|
|
220
252
|
// eslint-disable-next-line no-restricted-syntax
|
|
221
253
|
export const emailSchema = yupString().email();
|
|
254
|
+
import.meta.vitest?.test('strictEmailSchema', ({ expect }) => {
|
|
255
|
+
const validEmails = [
|
|
256
|
+
"a@example.com",
|
|
257
|
+
"abc@example.com",
|
|
258
|
+
"a.b@example.com",
|
|
259
|
+
"throwaway.mail+token@example.com",
|
|
260
|
+
"email-alt-dash@demo-mail.com",
|
|
261
|
+
"test-account@weird-domain.net",
|
|
262
|
+
"%!~&+{}=|`#@domain.test",
|
|
263
|
+
"admin@a.longtldexample",
|
|
264
|
+
];
|
|
265
|
+
for (const email of validEmails) {
|
|
266
|
+
expect(strictEmailSchema(undefined).validateSync(email)).toBe(email);
|
|
267
|
+
}
|
|
268
|
+
const invalidEmails = [
|
|
269
|
+
"test@localhost",
|
|
270
|
+
"test@gmail",
|
|
271
|
+
"test@gmail.com.a",
|
|
272
|
+
"test@gmail.a",
|
|
273
|
+
"test.@example.com",
|
|
274
|
+
"test..test@example.com",
|
|
275
|
+
".test@example.com",
|
|
276
|
+
];
|
|
277
|
+
for (const email of invalidEmails) {
|
|
278
|
+
expect(() => strictEmailSchema(undefined).validateSync(email)).toThrow();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
222
281
|
// Request auth
|
|
223
282
|
export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']).defined();
|
|
224
283
|
export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']).defined();
|
package/dist/utils/api-keys.js
CHANGED
|
@@ -8,7 +8,8 @@ const API_KEY_LENGTHS = {
|
|
|
8
8
|
SECRET_PART: 45,
|
|
9
9
|
ID_PART: 32,
|
|
10
10
|
TYPE_PART: 4,
|
|
11
|
-
|
|
11
|
+
SCANNER: 1,
|
|
12
|
+
MARKER: 9,
|
|
12
13
|
CHECKSUM: 8,
|
|
13
14
|
};
|
|
14
15
|
function createChecksumSync(checksummablePart) {
|
|
@@ -27,22 +28,22 @@ function createApiKeyParts(options) {
|
|
|
27
28
|
return { checksummablePart, idPart, prefix, scannerAndMarker, type };
|
|
28
29
|
}
|
|
29
30
|
function parseApiKeyParts(secret) {
|
|
30
|
-
const regex = new RegExp(`^([
|
|
31
|
-
`(
|
|
32
|
-
`(
|
|
33
|
-
`(
|
|
34
|
-
`(
|
|
35
|
-
`(
|
|
31
|
+
const regex = new RegExp(`^([a-zA-Z0-9_]+)_` + // prefix
|
|
32
|
+
`([a-zA-Z0-9_]{${API_KEY_LENGTHS.SECRET_PART}})` + // secretPart
|
|
33
|
+
`([a-zA-Z0-9_]{${API_KEY_LENGTHS.ID_PART}})` + // idPart
|
|
34
|
+
`([a-zA-Z0-9_]{${API_KEY_LENGTHS.TYPE_PART}})` + // type
|
|
35
|
+
`([a-zA-Z0-9_]{${API_KEY_LENGTHS.SCANNER}})` + // scanner
|
|
36
|
+
`(${STACK_AUTH_MARKER})` + // marker
|
|
37
|
+
`([a-zA-Z0-9_]{${API_KEY_LENGTHS.CHECKSUM}})$` // checksum
|
|
36
38
|
);
|
|
37
39
|
const match = secret.match(regex);
|
|
38
40
|
if (!match) {
|
|
39
41
|
throw new StackAssertionError("Invalid API key format");
|
|
40
42
|
}
|
|
41
|
-
const [, prefix, secretPart, idPart, type,
|
|
42
|
-
const scannerFlag = scannerAndMarker.replace(STACK_AUTH_MARKER, "");
|
|
43
|
+
const [, prefix, secretPart, idPart, type, scannerFlag, marker, checksum] = match;
|
|
43
44
|
const isCloudVersion = parseInt(scannerFlag, 32) % 2 === 0;
|
|
44
45
|
const isPublic = (parseInt(scannerFlag, 32) & 2) !== 0;
|
|
45
|
-
const checksummablePart = `${prefix}_${secretPart}${idPart}${type}${
|
|
46
|
+
const checksummablePart = `${prefix}_${secretPart}${idPart}${type}${scannerFlag}${marker}`;
|
|
46
47
|
const restored_id = idPart.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5");
|
|
47
48
|
if (!["user", "team"].includes(type)) {
|
|
48
49
|
throw new StackAssertionError("Invalid type");
|
package/dist/utils/caches.d.ts
CHANGED
|
@@ -17,16 +17,16 @@ export declare class AsyncCache<D extends any[], T> {
|
|
|
17
17
|
refreshWhere(predicate: (dependencies: D) => boolean): Promise<void>;
|
|
18
18
|
readonly isCacheAvailable: (key: D) => boolean;
|
|
19
19
|
readonly getIfCached: (key: D) => ({
|
|
20
|
-
status: "error";
|
|
21
|
-
error: unknown;
|
|
22
|
-
} & {
|
|
23
|
-
status: "error";
|
|
24
|
-
}) | ({
|
|
25
20
|
status: "pending";
|
|
26
21
|
} & {
|
|
27
22
|
progress: void;
|
|
28
23
|
} & {
|
|
29
24
|
status: "pending";
|
|
25
|
+
}) | ({
|
|
26
|
+
status: "error";
|
|
27
|
+
error: unknown;
|
|
28
|
+
} & {
|
|
29
|
+
status: "error";
|
|
30
30
|
}) | ({
|
|
31
31
|
status: "ok";
|
|
32
32
|
data: T;
|
|
@@ -57,16 +57,16 @@ declare class AsyncValueCache<T> {
|
|
|
57
57
|
});
|
|
58
58
|
isCacheAvailable(): boolean;
|
|
59
59
|
getIfCached(): ({
|
|
60
|
-
status: "error";
|
|
61
|
-
error: unknown;
|
|
62
|
-
} & {
|
|
63
|
-
status: "error";
|
|
64
|
-
}) | ({
|
|
65
60
|
status: "pending";
|
|
66
61
|
} & {
|
|
67
62
|
progress: void;
|
|
68
63
|
} & {
|
|
69
64
|
status: "pending";
|
|
65
|
+
}) | ({
|
|
66
|
+
status: "error";
|
|
67
|
+
error: unknown;
|
|
68
|
+
} & {
|
|
69
|
+
status: "error";
|
|
70
70
|
}) | ({
|
|
71
71
|
status: "ok";
|
|
72
72
|
data: T;
|
package/dist/utils/objects.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ export declare function isNotNull<T>(value: T): value is NonNullable<T>;
|
|
|
2
2
|
export type DeepPartial<T> = T extends object ? {
|
|
3
3
|
[P in keyof T]?: DeepPartial<T[P]>;
|
|
4
4
|
} : T;
|
|
5
|
+
export type DeepRequired<T> = T extends object ? {
|
|
6
|
+
[P in keyof T]-?: DeepRequired<T[P]>;
|
|
7
|
+
} : T;
|
|
5
8
|
/**
|
|
6
9
|
* Assumes both objects are primitives, arrays, or non-function plain objects, and compares them deeply.
|
|
7
10
|
*
|
|
@@ -10,9 +13,16 @@ export type DeepPartial<T> = T extends object ? {
|
|
|
10
13
|
export declare function deepPlainEquals<T>(obj1: T, obj2: unknown, options?: {
|
|
11
14
|
ignoreUndefinedValues?: boolean;
|
|
12
15
|
}): obj2 is T;
|
|
16
|
+
export declare function isCloneable<T>(obj: T): obj is Exclude<T, symbol | Function>;
|
|
17
|
+
export declare function shallowClone<T extends object>(obj: T): T;
|
|
13
18
|
export declare function deepPlainClone<T>(obj: T): T;
|
|
19
|
+
export type DeepMerge<T, U> = Omit<T, keyof U> & Omit<U, keyof T> & DeepMergeInner<Pick<T, keyof U & keyof T>, Pick<U, keyof U & keyof T>>;
|
|
20
|
+
type DeepMergeInner<T, U> = {
|
|
21
|
+
[K in keyof U]-?: undefined extends U[K] ? K extends keyof T ? T[K] extends object ? Exclude<U[K], undefined> extends object ? DeepMerge<T[K], Exclude<U[K], undefined>> : T[K] | Exclude<U[K], undefined> : T[K] | Exclude<U[K], undefined> : Exclude<U[K], undefined> : K extends keyof T ? T[K] extends object ? U[K] extends object ? DeepMerge<T[K], U[K]> : U[K] : U[K] : U[K];
|
|
22
|
+
};
|
|
23
|
+
export declare function deepMerge<T extends {}, U extends {}>(baseObj: T, mergeObj: U): DeepMerge<T, U>;
|
|
14
24
|
export declare function typedEntries<T extends {}>(obj: T): [keyof T, T[keyof T]][];
|
|
15
|
-
export declare function typedFromEntries<K extends PropertyKey, V>(entries: [K, V][]): Record<K, V>;
|
|
25
|
+
export declare function typedFromEntries<K extends PropertyKey, V>(entries: (readonly [K, V])[]): Record<K, V>;
|
|
16
26
|
export declare function typedKeys<T extends {}>(obj: T): (keyof T)[];
|
|
17
27
|
export declare function typedValues<T extends {}>(obj: T): T[keyof T][];
|
|
18
28
|
export declare function typedAssign<T extends {}, U extends {}>(target: T, source: U): T & U;
|
|
@@ -25,7 +35,7 @@ export type FilterUndefined<T> = {
|
|
|
25
35
|
* Returns a new object with all undefined values removed. Useful when spreading optional parameters on an object, as
|
|
26
36
|
* TypeScript's `Partial<XYZ>` type allows `undefined` values.
|
|
27
37
|
*/
|
|
28
|
-
export declare function filterUndefined<T extends
|
|
38
|
+
export declare function filterUndefined<T extends object>(obj: T): FilterUndefined<T>;
|
|
29
39
|
export type FilterUndefinedOrNull<T> = FilterUndefined<{
|
|
30
40
|
[k in keyof T]: null extends T[k] ? NonNullable<T[k]> | undefined : T[k];
|
|
31
41
|
}>;
|
|
@@ -33,7 +43,22 @@ export type FilterUndefinedOrNull<T> = FilterUndefined<{
|
|
|
33
43
|
* Returns a new object with all undefined and null values removed. Useful when spreading optional parameters on an object, as
|
|
34
44
|
* TypeScript's `Partial<XYZ>` type allows `undefined` values.
|
|
35
45
|
*/
|
|
36
|
-
export declare function filterUndefinedOrNull<T extends
|
|
46
|
+
export declare function filterUndefinedOrNull<T extends object>(obj: T): FilterUndefinedOrNull<T>;
|
|
47
|
+
export type DeepFilterUndefined<T> = T extends object ? FilterUndefined<{
|
|
48
|
+
[K in keyof T]: DeepFilterUndefined<T[K]>;
|
|
49
|
+
}> : T;
|
|
50
|
+
export declare function deepFilterUndefined<T extends object>(obj: T): DeepFilterUndefined<T>;
|
|
37
51
|
export declare function pick<T extends {}, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>;
|
|
38
52
|
export declare function omit<T extends {}, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>;
|
|
39
53
|
export declare function split<T extends {}, K extends keyof T>(obj: T, keys: K[]): [Pick<T, K>, Omit<T, K>];
|
|
54
|
+
export declare function set<T extends object, K extends keyof T>(obj: T, key: K, value: T[K]): void;
|
|
55
|
+
export declare function get<T extends object, K extends keyof T>(obj: T, key: K): T[K];
|
|
56
|
+
export declare function has<T extends object, K extends keyof T>(obj: T, key: K): obj is T & {
|
|
57
|
+
[k in K]: unknown;
|
|
58
|
+
};
|
|
59
|
+
export declare function hasAndNotUndefined<T extends object, K extends keyof T>(obj: T, key: K): obj is T & {
|
|
60
|
+
[k in K]: Exclude<T[K], undefined>;
|
|
61
|
+
};
|
|
62
|
+
export declare function deleteKey<T extends object, K extends keyof T>(obj: T, key: K): void;
|
|
63
|
+
export declare function isObjectLike(value: unknown): value is object;
|
|
64
|
+
export {};
|
package/dist/utils/objects.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StackAssertionError } from "./errors";
|
|
2
|
+
import { identity } from "./functions";
|
|
2
3
|
export function isNotNull(value) {
|
|
3
4
|
return value !== null && value !== undefined;
|
|
4
5
|
}
|
|
@@ -78,6 +79,21 @@ import.meta.vitest?.test("deepPlainEquals", ({ expect }) => {
|
|
|
78
79
|
expect(deepPlainEquals({ a: 1, b: undefined }, { a: 1 }, { ignoreUndefinedValues: true })).toBe(true);
|
|
79
80
|
expect(deepPlainEquals({ a: 1, b: undefined }, { a: 1 })).toBe(false);
|
|
80
81
|
});
|
|
82
|
+
export function isCloneable(obj) {
|
|
83
|
+
return typeof obj !== 'symbol' && typeof obj !== 'function';
|
|
84
|
+
}
|
|
85
|
+
export function shallowClone(obj) {
|
|
86
|
+
if (!isCloneable(obj))
|
|
87
|
+
throw new StackAssertionError("shallowClone does not support symbols or functions", { obj });
|
|
88
|
+
if (Array.isArray(obj))
|
|
89
|
+
return obj.map(identity);
|
|
90
|
+
return { ...obj };
|
|
91
|
+
}
|
|
92
|
+
import.meta.vitest?.test("shallowClone", ({ expect }) => {
|
|
93
|
+
expect(shallowClone({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
|
|
94
|
+
expect(shallowClone([1, 2, 3])).toEqual([1, 2, 3]);
|
|
95
|
+
expect(() => shallowClone(() => { })).toThrow();
|
|
96
|
+
});
|
|
81
97
|
export function deepPlainClone(obj) {
|
|
82
98
|
if (typeof obj === 'function')
|
|
83
99
|
throw new StackAssertionError("deepPlainClone does not support functions");
|
|
@@ -116,6 +132,75 @@ import.meta.vitest?.test("deepPlainClone", ({ expect }) => {
|
|
|
116
132
|
expect(() => deepPlainClone(() => { })).toThrow();
|
|
117
133
|
expect(() => deepPlainClone(Symbol())).toThrow();
|
|
118
134
|
});
|
|
135
|
+
export function deepMerge(baseObj, mergeObj) {
|
|
136
|
+
if ([baseObj, mergeObj, ...Object.values(baseObj), ...Object.values(mergeObj)].some(o => !isCloneable(o)))
|
|
137
|
+
throw new StackAssertionError("deepMerge does not support functions or symbols", { baseObj, mergeObj });
|
|
138
|
+
const res = shallowClone(baseObj);
|
|
139
|
+
for (const [key, mergeValue] of Object.entries(mergeObj)) {
|
|
140
|
+
if (has(res, key)) {
|
|
141
|
+
const baseValue = get(res, key);
|
|
142
|
+
if (isObjectLike(baseValue) && isObjectLike(mergeValue)) {
|
|
143
|
+
set(res, key, deepMerge(baseValue, mergeValue));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
set(res, key, mergeValue);
|
|
148
|
+
}
|
|
149
|
+
return res;
|
|
150
|
+
}
|
|
151
|
+
import.meta.vitest?.test("deepMerge", ({ expect }) => {
|
|
152
|
+
// Test merging flat objects
|
|
153
|
+
expect(deepMerge({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
|
|
154
|
+
expect(deepMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
|
|
155
|
+
expect(deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 })).toEqual({ a: 1, b: 3, c: 4 });
|
|
156
|
+
// Test with nested objects
|
|
157
|
+
expect(deepMerge({ a: { x: 1, y: 2 }, b: 3 }, { a: { y: 3, z: 4 }, c: 5 })).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 3, c: 5 });
|
|
158
|
+
// Test with arrays
|
|
159
|
+
expect(deepMerge({ a: [1, 2], b: 3 }, { a: [3, 4], c: 5 })).toEqual({ a: [3, 4], b: 3, c: 5 });
|
|
160
|
+
// Test with null values
|
|
161
|
+
expect(deepMerge({ a: { x: 1 }, b: null }, { a: { y: 2 }, b: { z: 3 } })).toEqual({ a: { x: 1, y: 2 }, b: { z: 3 } });
|
|
162
|
+
// Test with undefined values
|
|
163
|
+
expect(deepMerge({ a: 1, b: undefined }, { b: 2, c: 3 })).toEqual({ a: 1, b: 2, c: 3 });
|
|
164
|
+
// Test deeply nested structures
|
|
165
|
+
expect(deepMerge({
|
|
166
|
+
a: {
|
|
167
|
+
x: { deep: 1 },
|
|
168
|
+
y: [1, 2]
|
|
169
|
+
},
|
|
170
|
+
b: 2
|
|
171
|
+
}, {
|
|
172
|
+
a: {
|
|
173
|
+
x: { deeper: 3 },
|
|
174
|
+
y: [3, 4]
|
|
175
|
+
},
|
|
176
|
+
c: 3
|
|
177
|
+
})).toEqual({
|
|
178
|
+
a: {
|
|
179
|
+
x: { deep: 1, deeper: 3 },
|
|
180
|
+
y: [3, 4]
|
|
181
|
+
},
|
|
182
|
+
b: 2,
|
|
183
|
+
c: 3
|
|
184
|
+
});
|
|
185
|
+
// Test with empty objects
|
|
186
|
+
expect(deepMerge({}, { a: 1 })).toEqual({ a: 1 });
|
|
187
|
+
expect(deepMerge({ a: 1 }, {})).toEqual({ a: 1 });
|
|
188
|
+
expect(deepMerge({}, {})).toEqual({});
|
|
189
|
+
// Test that original objects are not modified
|
|
190
|
+
const base = { a: { x: 1 }, b: 2 };
|
|
191
|
+
const merge = { a: { y: 2 }, c: 3 };
|
|
192
|
+
const baseClone = deepPlainClone(base);
|
|
193
|
+
const mergeClone = deepPlainClone(merge);
|
|
194
|
+
const result = deepMerge(base, merge);
|
|
195
|
+
expect(base).toEqual(baseClone);
|
|
196
|
+
expect(merge).toEqual(mergeClone);
|
|
197
|
+
expect(result).toEqual({ a: { x: 1, y: 2 }, b: 2, c: 3 });
|
|
198
|
+
// Test error cases
|
|
199
|
+
expect(() => deepMerge({ a: () => { } }, { b: 2 })).toThrow();
|
|
200
|
+
expect(() => deepMerge({ a: 1 }, { b: () => { } })).toThrow();
|
|
201
|
+
expect(() => deepMerge({ a: Symbol() }, { b: 2 })).toThrow();
|
|
202
|
+
expect(() => deepMerge({ a: 1 }, { b: Symbol() })).toThrow();
|
|
203
|
+
});
|
|
119
204
|
export function typedEntries(obj) {
|
|
120
205
|
return Object.entries(obj);
|
|
121
206
|
}
|
|
@@ -226,6 +311,12 @@ import.meta.vitest?.test("filterUndefinedOrNull", ({ expect }) => {
|
|
|
226
311
|
expect(filterUndefinedOrNull({})).toEqual({});
|
|
227
312
|
expect(filterUndefinedOrNull({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
|
|
228
313
|
});
|
|
314
|
+
export function deepFilterUndefined(obj) {
|
|
315
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined).map(([k, v]) => [k, isObjectLike(v) ? deepFilterUndefined(v) : v]));
|
|
316
|
+
}
|
|
317
|
+
import.meta.vitest?.test("deepFilterUndefined", ({ expect }) => {
|
|
318
|
+
expect(deepFilterUndefined({ a: 1, b: undefined })).toEqual({ a: 1 });
|
|
319
|
+
});
|
|
229
320
|
export function pick(obj, keys) {
|
|
230
321
|
return Object.fromEntries(Object.entries(obj).filter(([k]) => keys.includes(k)));
|
|
231
322
|
}
|
|
@@ -259,3 +350,29 @@ import.meta.vitest?.test("split", ({ expect }) => {
|
|
|
259
350
|
// Use type assertion for empty object to avoid TypeScript error
|
|
260
351
|
expect(split({}, ["a"])).toEqual([{}, {}]);
|
|
261
352
|
});
|
|
353
|
+
export function set(obj, key, value) {
|
|
354
|
+
Object.defineProperty(obj, key, { value, writable: true, configurable: true, enumerable: true });
|
|
355
|
+
}
|
|
356
|
+
export function get(obj, key) {
|
|
357
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
358
|
+
if (!descriptor)
|
|
359
|
+
throw new StackAssertionError(`get: key ${String(key)} does not exist`, { obj, key });
|
|
360
|
+
return descriptor.value;
|
|
361
|
+
}
|
|
362
|
+
export function has(obj, key) {
|
|
363
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
364
|
+
}
|
|
365
|
+
export function hasAndNotUndefined(obj, key) {
|
|
366
|
+
return has(obj, key) && get(obj, key) !== undefined;
|
|
367
|
+
}
|
|
368
|
+
export function deleteKey(obj, key) {
|
|
369
|
+
if (has(obj, key)) {
|
|
370
|
+
Reflect.deleteProperty(obj, key);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
throw new StackAssertionError(`deleteKey: key ${String(key)} does not exist`, { obj, key });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
export function isObjectLike(value) {
|
|
377
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null;
|
|
378
|
+
}
|
package/dist/utils/promises.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare function pending<T>(promise: Promise<T>, options?: {
|
|
|
31
31
|
*
|
|
32
32
|
* Vercel kills serverless functions on unhandled promise rejection errors, so this is important.
|
|
33
33
|
*/
|
|
34
|
-
export declare function ignoreUnhandledRejection<T extends Promise<any>>(promise: T):
|
|
34
|
+
export declare function ignoreUnhandledRejection<T extends Promise<any>>(promise: T): void;
|
|
35
35
|
export declare function wait(ms: number): Promise<void>;
|
|
36
36
|
export declare function waitUntil(date: Date): Promise<void>;
|
|
37
37
|
export declare function runAsynchronouslyWithAlert(...args: Parameters<typeof runAsynchronously>): void;
|
package/dist/utils/promises.js
CHANGED
|
@@ -114,7 +114,9 @@ export function rejected(reason) {
|
|
|
114
114
|
if (rejectedCache.has([reason])) {
|
|
115
115
|
return rejectedCache.get([reason]);
|
|
116
116
|
}
|
|
117
|
-
const
|
|
117
|
+
const promise = Promise.reject(reason);
|
|
118
|
+
ignoreUnhandledRejection(promise);
|
|
119
|
+
const res = Object.assign(promise, {
|
|
118
120
|
status: "rejected",
|
|
119
121
|
reason: reason,
|
|
120
122
|
});
|
|
@@ -191,22 +193,19 @@ import.meta.vitest?.test("pending", async ({ expect }) => {
|
|
|
191
193
|
*/
|
|
192
194
|
export function ignoreUnhandledRejection(promise) {
|
|
193
195
|
promise.catch(() => { });
|
|
194
|
-
return promise;
|
|
195
196
|
}
|
|
196
197
|
import.meta.vitest?.test("ignoreUnhandledRejection", async ({ expect }) => {
|
|
197
198
|
// Test with a promise that resolves
|
|
198
199
|
const resolvePromise = Promise.resolve(42);
|
|
199
|
-
|
|
200
|
-
expect(
|
|
201
|
-
expect(await ignoredResolvePromise).toBe(42); // Should still resolve to the same value
|
|
200
|
+
ignoreUnhandledRejection(resolvePromise);
|
|
201
|
+
expect(await resolvePromise).toBe(42); // Should still resolve to the same value
|
|
202
202
|
// Test with a promise that rejects
|
|
203
|
-
const error = new Error("Test error");
|
|
204
|
-
const rejectPromise = Promise.reject(error);
|
|
205
|
-
const ignoredRejectPromise = ignoreUnhandledRejection(rejectPromise);
|
|
206
|
-
expect(ignoredRejectPromise).toBe(rejectPromise); // Should return the same promise
|
|
207
203
|
// The promise should still reject, but the rejection is caught internally
|
|
208
204
|
// so it doesn't cause an unhandled rejection error
|
|
209
|
-
|
|
205
|
+
const error = new Error("Test error");
|
|
206
|
+
const rejectPromise = Promise.reject(error);
|
|
207
|
+
ignoreUnhandledRejection(rejectPromise);
|
|
208
|
+
await expect(rejectPromise).rejects.toBe(error);
|
|
210
209
|
});
|
|
211
210
|
export async function wait(ms) {
|
|
212
211
|
if (!Number.isFinite(ms) || ms < 0) {
|
package/dist/utils/stores.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ export declare class AsyncStore<T> implements ReadonlyAsyncStore<T> {
|
|
|
60
60
|
isAvailable(): boolean;
|
|
61
61
|
isRejected(): boolean;
|
|
62
62
|
get(): ({
|
|
63
|
+
status: "pending";
|
|
64
|
+
} & {
|
|
65
|
+
progress: void;
|
|
66
|
+
} & {
|
|
67
|
+
status: "pending";
|
|
68
|
+
}) | ({
|
|
63
69
|
status: "error";
|
|
64
70
|
error: unknown;
|
|
65
71
|
} & {
|
|
@@ -69,12 +75,6 @@ export declare class AsyncStore<T> implements ReadonlyAsyncStore<T> {
|
|
|
69
75
|
data: T;
|
|
70
76
|
} & {
|
|
71
77
|
status: "ok";
|
|
72
|
-
}) | ({
|
|
73
|
-
status: "pending";
|
|
74
|
-
} & {
|
|
75
|
-
progress: void;
|
|
76
|
-
} & {
|
|
77
|
-
status: "pending";
|
|
78
78
|
});
|
|
79
79
|
getOrWait(): ReactPromise<T>;
|
|
80
80
|
_setIfLatest(result: Result<T>, curCounter: number): boolean;
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ export type IfAndOnlyIf<Value, Extends, Then, Otherwise> = (Value extends Extend
|
|
|
16
16
|
/**
|
|
17
17
|
* Can be used to prettify a type in the IDE; for example, some complicated intersected types can be flattened into a single type.
|
|
18
18
|
*/
|
|
19
|
-
export type PrettifyType<T> = {
|
|
19
|
+
export type PrettifyType<T> = T extends object ? {
|
|
20
20
|
[K in keyof T]: T[K];
|
|
21
|
-
} & {};
|
|
21
|
+
} & {} : T;
|