@flaredev/core 0.9.1 → 0.10.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/dist/{chunk-AAXNSIZG.js → chunk-TB53VHXY.js} +67 -1
- package/dist/index.d.ts +99 -2
- package/dist/index.js +41 -2
- package/dist/server.js +8 -1
- package/package.json +1 -1
|
@@ -91,6 +91,60 @@ var field = {
|
|
|
91
91
|
hasMany: (target, options) => ({ kind: "hasMany", target, ...options, required: false })
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
// src/resource/money.ts
|
|
95
|
+
var EXPONENTS = {
|
|
96
|
+
// No minor unit at all.
|
|
97
|
+
BIF: 0,
|
|
98
|
+
CLP: 0,
|
|
99
|
+
DJF: 0,
|
|
100
|
+
GNF: 0,
|
|
101
|
+
ISK: 0,
|
|
102
|
+
JPY: 0,
|
|
103
|
+
KMF: 0,
|
|
104
|
+
KRW: 0,
|
|
105
|
+
PYG: 0,
|
|
106
|
+
RWF: 0,
|
|
107
|
+
UGX: 0,
|
|
108
|
+
UYI: 0,
|
|
109
|
+
VND: 0,
|
|
110
|
+
VUV: 0,
|
|
111
|
+
XAF: 0,
|
|
112
|
+
XOF: 0,
|
|
113
|
+
XPF: 0,
|
|
114
|
+
// Thousandths.
|
|
115
|
+
BHD: 3,
|
|
116
|
+
IQD: 3,
|
|
117
|
+
JOD: 3,
|
|
118
|
+
KWD: 3,
|
|
119
|
+
LYD: 3,
|
|
120
|
+
OMR: 3,
|
|
121
|
+
TND: 3,
|
|
122
|
+
// Ten-thousandths.
|
|
123
|
+
CLF: 4
|
|
124
|
+
};
|
|
125
|
+
var DEFAULT_MONEY_EXPONENT = 2;
|
|
126
|
+
function moneyExponent(currency) {
|
|
127
|
+
if (!currency) return DEFAULT_MONEY_EXPONENT;
|
|
128
|
+
return EXPONENTS[currency.toUpperCase()] ?? DEFAULT_MONEY_EXPONENT;
|
|
129
|
+
}
|
|
130
|
+
var factor = (exponent) => 10 ** exponent;
|
|
131
|
+
function toMinorUnits(amount, currency) {
|
|
132
|
+
if (!Number.isFinite(amount)) throw new TypeError(`Not an amount: ${amount}`);
|
|
133
|
+
return Number(amount.toFixed(moneyExponent(currency)).replace(".", ""));
|
|
134
|
+
}
|
|
135
|
+
function fromMinorUnits(minor, currency) {
|
|
136
|
+
const exponent = moneyExponent(currency);
|
|
137
|
+
if (exponent === 0) return minor;
|
|
138
|
+
return Number((minor / factor(exponent)).toFixed(exponent));
|
|
139
|
+
}
|
|
140
|
+
function hasTooManyPlaces(amount, currency) {
|
|
141
|
+
const exponent = moneyExponent(currency);
|
|
142
|
+
const text = String(amount);
|
|
143
|
+
if (text.includes("e") || text.includes("E")) return true;
|
|
144
|
+
const places = text.includes(".") ? text.length - text.indexOf(".") - 1 : 0;
|
|
145
|
+
return places > exponent;
|
|
146
|
+
}
|
|
147
|
+
|
|
94
148
|
// src/resource/naming.ts
|
|
95
149
|
function words(value) {
|
|
96
150
|
return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((word) => word.toLowerCase());
|
|
@@ -367,7 +421,14 @@ function fieldSchema(def) {
|
|
|
367
421
|
if (def.format === "rating") {
|
|
368
422
|
s = s.min(def.min ?? 0, `At least ${def.min ?? 0}`).max(def.max ?? 5, `At most ${def.max ?? 5}`);
|
|
369
423
|
}
|
|
370
|
-
if (def.format === "money"
|
|
424
|
+
if (def.format === "money") {
|
|
425
|
+
if (def.min === void 0) s = s.min(0, "Cannot be negative");
|
|
426
|
+
const places = moneyExponent(def.currency);
|
|
427
|
+
s = s.refine(
|
|
428
|
+
(value) => !hasTooManyPlaces(value, def.currency),
|
|
429
|
+
places === 0 ? "A whole amount, with no decimal places" : `At most ${places} decimal place${places === 1 ? "" : "s"}`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
371
432
|
if (def.min !== void 0) s = s.min(def.min, `Must be at least ${def.min}`);
|
|
372
433
|
if (def.max !== void 0) s = s.max(def.max, `Must be at most ${def.max}`);
|
|
373
434
|
return s;
|
|
@@ -501,6 +562,11 @@ export {
|
|
|
501
562
|
DEFAULT_FILE_MAX_BYTES,
|
|
502
563
|
fileMaxBytes,
|
|
503
564
|
field,
|
|
565
|
+
DEFAULT_MONEY_EXPONENT,
|
|
566
|
+
moneyExponent,
|
|
567
|
+
toMinorUnits,
|
|
568
|
+
fromMinorUnits,
|
|
569
|
+
hasTooManyPlaces,
|
|
504
570
|
words,
|
|
505
571
|
pascalCase,
|
|
506
572
|
camelCase,
|
package/dist/index.d.ts
CHANGED
|
@@ -285,6 +285,52 @@ declare const checkoutPaid: (session: {
|
|
|
285
285
|
/** `8 random lowercase letters`, the suffix Stripe asks for on `integration_identifier`. */
|
|
286
286
|
declare function randomLetters(length?: number): string;
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Money, stored exactly.
|
|
290
|
+
*
|
|
291
|
+
* A `money` field used to be a `float`, and `0.1 + 0.2` is the reason that was a bug
|
|
292
|
+
* waiting for a shop with a till in it. Binary floating point cannot represent most
|
|
293
|
+
* decimal fractions, so a column of them drifts: sum a few thousand prices and the total
|
|
294
|
+
* is wrong by a cent, which is the kind of wrong nobody can explain to an accountant.
|
|
295
|
+
*
|
|
296
|
+
* So the column holds **minor units** — whole cents, whole fils, whole yen — and an
|
|
297
|
+
* integer column is exact. The store converts at the boundary, so everything above it
|
|
298
|
+
* still works in the units people write and read: the API sends 19.99, the form shows
|
|
299
|
+
* 19.99, and only the database sees 1999.
|
|
300
|
+
*
|
|
301
|
+
* Flare's own billing code has always done this (`amount:int`, formatted as `cents / 100`).
|
|
302
|
+
* This brings `money` fields in line with it.
|
|
303
|
+
*/
|
|
304
|
+
/** The default when a field names no currency: two places, which is almost everywhere. */
|
|
305
|
+
declare const DEFAULT_MONEY_EXPONENT = 2;
|
|
306
|
+
/** Decimal places for an ISO 4217 code. Unknown codes get two, which is the common case. */
|
|
307
|
+
declare function moneyExponent(currency?: string): number;
|
|
308
|
+
/**
|
|
309
|
+
* A decimal amount as whole minor units: 19.99 → 1999.
|
|
310
|
+
*
|
|
311
|
+
* Through `toFixed` rather than `Math.round(amount * 100)`, because multiplying first adds
|
|
312
|
+
* an error of its own: `2.675 * 100` is 267.50000000000006, which rounds to 268, while the
|
|
313
|
+
* double behind `2.675` is 2.67499999999999982 and rounds to 267. `toFixed` asks the
|
|
314
|
+
* runtime to round the value that is actually there, so this agrees with what every other
|
|
315
|
+
* tool prints for the same number.
|
|
316
|
+
*
|
|
317
|
+
* Neither approach can rescue a value the double never held — `1.005` is 1.00499999999999989
|
|
318
|
+
* and becomes 100, not 101. Nothing short of decimal arithmetic all the way from the client
|
|
319
|
+
* fixes that, which is why the validator refuses an amount with more places than its
|
|
320
|
+
* currency has: the place to catch it is before it is a double at all.
|
|
321
|
+
*/
|
|
322
|
+
declare function toMinorUnits(amount: number, currency?: string): number;
|
|
323
|
+
/** Whole minor units as a decimal amount: 1999 → 19.99. */
|
|
324
|
+
declare function fromMinorUnits(minor: number, currency?: string): number;
|
|
325
|
+
/**
|
|
326
|
+
* Whether an amount has more decimal places than its currency has.
|
|
327
|
+
*
|
|
328
|
+
* 19.999 in a two-place currency is not a rounding problem to solve quietly — it is a
|
|
329
|
+
* caller sending something they did not mean, and storing 20.00 instead would be a silent
|
|
330
|
+
* change to a number that matters.
|
|
331
|
+
*/
|
|
332
|
+
declare function hasTooManyPlaces(amount: number, currency?: string): boolean;
|
|
333
|
+
|
|
288
334
|
/**
|
|
289
335
|
* Zod schemas derived from a descriptor at runtime. Unknown keys are rejected,
|
|
290
336
|
* so clients can't set `id`, timestamps or undeclared columns (no mass assignment).
|
|
@@ -525,10 +571,61 @@ interface SeedContext {
|
|
|
525
571
|
insertMany: InsertMany;
|
|
526
572
|
/** Sample values: names, emails, sentences, dates. */
|
|
527
573
|
fake: Fake;
|
|
574
|
+
/**
|
|
575
|
+
* Creates a user with a credential account, so you can sign in as them straight away:
|
|
576
|
+
*
|
|
577
|
+
* await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
|
|
578
|
+
*/
|
|
579
|
+
seedUser: SeedUser;
|
|
580
|
+
}
|
|
581
|
+
/** What `seedUser` needs to make somebody who can sign in. */
|
|
582
|
+
interface SeedUserInput {
|
|
583
|
+
email: string;
|
|
584
|
+
/** Stored as the same PBKDF2 hash the sign-up route writes, so sign-in verifies it. */
|
|
585
|
+
password: string;
|
|
586
|
+
name?: string;
|
|
587
|
+
/** Seeded users are verified by default: an unverified one cannot sign in. */
|
|
588
|
+
emailVerified?: boolean;
|
|
589
|
+
/** e.g. "admin", which the dashboard's policies read. */
|
|
590
|
+
role?: string;
|
|
528
591
|
}
|
|
592
|
+
/** A user row and the credential account row that lets them sign in. */
|
|
593
|
+
interface CredentialUserRows {
|
|
594
|
+
user: {
|
|
595
|
+
id: string;
|
|
596
|
+
name: string;
|
|
597
|
+
email: string;
|
|
598
|
+
emailVerified: boolean;
|
|
599
|
+
role: string | undefined;
|
|
600
|
+
createdAt: Date;
|
|
601
|
+
updatedAt: Date;
|
|
602
|
+
};
|
|
603
|
+
account: {
|
|
604
|
+
id: string;
|
|
605
|
+
accountId: string;
|
|
606
|
+
providerId: string;
|
|
607
|
+
userId: string;
|
|
608
|
+
password: string;
|
|
609
|
+
createdAt: Date;
|
|
610
|
+
updatedAt: Date;
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* The two rows Better Auth looks for when somebody signs in with a password.
|
|
615
|
+
*
|
|
616
|
+
* `accountId` is the one that is easy to get wrong. It is not the provider's name and it is
|
|
617
|
+
* not the email: for a credential account Better Auth writes the user's own id into that
|
|
618
|
+
* column at sign-up, and looks the account up by it at sign-in. Put the email there instead
|
|
619
|
+
* and everything looks right — the row exists, the hash is correct — but signing in answers
|
|
620
|
+
* "User not found", which is a long afternoon. Both rows are built here so that rule lives
|
|
621
|
+
* in one place rather than in every seed somebody writes by hand.
|
|
622
|
+
*/
|
|
623
|
+
declare function credentialUserRows(input: SeedUserInput): Promise<CredentialUserRows>;
|
|
624
|
+
/** Creates a user who can actually sign in, and returns the row. */
|
|
625
|
+
type SeedUser = (user: SeedUserInput) => Promise<CredentialUserRows["user"]>;
|
|
529
626
|
type Seed = (context: SeedContext) => void | Promise<void>;
|
|
530
627
|
declare function defineSeed(seed: Seed): Seed;
|
|
531
628
|
|
|
532
|
-
declare const FLARE_VERSION = "0.
|
|
629
|
+
declare const FLARE_VERSION = "0.10.0";
|
|
533
630
|
|
|
534
|
-
export { type BelongsToRelation, type Country, CreateInput, FLARE_VERSION, type Fake, Field, type FormValues, type FormatOptions, type HasManyRelation, type InsertMany, type MailMessage, type MailResult, type Mailer, type MailerOptions, PBKDF2_ITERATIONS, type ReadUrlOptions, RelationError, type RelationGraph, Resource, type ResourceRelations, type ResourceValidators, SNIFF_BYTES, SUBSCRIPTION_STATUSES, type Seed, type SeedContext, type Storage, type StorageBucket, type StorageOptions, StoredField, type SubscriptionLike, type SubscriptionState, type SubscriptionStatus, type Tone, type TransactionalEmail, UpdateInput, type UploadUrlOptions, camelCase, checkoutPaid, countries, countryName, createFake, createMailer, createObjectKey, createStorage, createValidators, currentPeriodEnd, defineSeed, fake, fieldSchema, fileKeyPrefix, flagEmoji, formValuesToInput, formatPhone, formatValue, grantsAccess, hashPassword, humanize, importSigningKey, initialFormValues, isColor, isCountryCode, isDomain, isLiveSubscription, isPhoneNumber, isSlug, issuesByField, kebabCase, matchesContentType, normalizeDomain, optionLabel, parseMultiValue, pascalCase, pluralize, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, slugify, snakeCase, sniffMatches, splitPhone, statusTone, subscriptionPriceId, subscriptionState, toE164, toSubscriptionStatus, verifyPassword, verifyToken, words };
|
|
631
|
+
export { type BelongsToRelation, type Country, CreateInput, type CredentialUserRows, DEFAULT_MONEY_EXPONENT, FLARE_VERSION, type Fake, Field, type FormValues, type FormatOptions, type HasManyRelation, type InsertMany, type MailMessage, type MailResult, type Mailer, type MailerOptions, PBKDF2_ITERATIONS, type ReadUrlOptions, RelationError, type RelationGraph, Resource, type ResourceRelations, type ResourceValidators, SNIFF_BYTES, SUBSCRIPTION_STATUSES, type Seed, type SeedContext, type SeedUser, type SeedUserInput, type Storage, type StorageBucket, type StorageOptions, StoredField, type SubscriptionLike, type SubscriptionState, type SubscriptionStatus, type Tone, type TransactionalEmail, UpdateInput, type UploadUrlOptions, camelCase, checkoutPaid, countries, countryName, createFake, createMailer, createObjectKey, createStorage, createValidators, credentialUserRows, currentPeriodEnd, defineSeed, fake, fieldSchema, fileKeyPrefix, flagEmoji, formValuesToInput, formatPhone, formatValue, fromMinorUnits, grantsAccess, hasTooManyPlaces, hashPassword, humanize, importSigningKey, initialFormValues, isColor, isCountryCode, isDomain, isLiveSubscription, isPhoneNumber, isSlug, issuesByField, kebabCase, matchesContentType, moneyExponent, normalizeDomain, optionLabel, parseMultiValue, pascalCase, pluralize, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, slugify, snakeCase, sniffMatches, splitPhone, statusTone, subscriptionPriceId, subscriptionState, toE164, toMinorUnits, toSubscriptionStatus, verifyPassword, verifyToken, words };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_FILE_MAX_BYTES,
|
|
3
|
+
DEFAULT_MONEY_EXPONENT,
|
|
3
4
|
FILE_CATEGORIES,
|
|
4
5
|
NUMBER_FORMATS,
|
|
5
6
|
PolicyError,
|
|
@@ -24,6 +25,8 @@ import {
|
|
|
24
25
|
fileMaxBytes,
|
|
25
26
|
flagEmoji,
|
|
26
27
|
formatPhone,
|
|
28
|
+
fromMinorUnits,
|
|
29
|
+
hasTooManyPlaces,
|
|
27
30
|
humanize,
|
|
28
31
|
isColor,
|
|
29
32
|
isCountryCode,
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
isSlug,
|
|
33
36
|
kebabCase,
|
|
34
37
|
mimeTypesFor,
|
|
38
|
+
moneyExponent,
|
|
35
39
|
normalizeDomain,
|
|
36
40
|
ownershipFilter,
|
|
37
41
|
pascalCase,
|
|
@@ -41,8 +45,9 @@ import {
|
|
|
41
45
|
splitPhone,
|
|
42
46
|
storedFields,
|
|
43
47
|
toE164,
|
|
48
|
+
toMinorUnits,
|
|
44
49
|
words
|
|
45
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-TB53VHXY.js";
|
|
46
51
|
|
|
47
52
|
// src/password.ts
|
|
48
53
|
var ALGORITHM = "pbkdf2-sha256";
|
|
@@ -705,6 +710,34 @@ function issuesByField(issues) {
|
|
|
705
710
|
}
|
|
706
711
|
|
|
707
712
|
// src/seed.ts
|
|
713
|
+
async function credentialUserRows(input) {
|
|
714
|
+
const email = input.email.trim().toLowerCase();
|
|
715
|
+
if (!email.includes("@")) throw new Error(`seedUser needs an email address, got "${input.email}".`);
|
|
716
|
+
if (!input.password) throw new Error(`seedUser needs a password for ${email}.`);
|
|
717
|
+
const id = crypto.randomUUID();
|
|
718
|
+
const now = /* @__PURE__ */ new Date();
|
|
719
|
+
return {
|
|
720
|
+
user: {
|
|
721
|
+
id,
|
|
722
|
+
name: input.name ?? email.split("@")[0],
|
|
723
|
+
email,
|
|
724
|
+
emailVerified: input.emailVerified ?? true,
|
|
725
|
+
role: input.role,
|
|
726
|
+
createdAt: now,
|
|
727
|
+
updatedAt: now
|
|
728
|
+
},
|
|
729
|
+
account: {
|
|
730
|
+
id: crypto.randomUUID(),
|
|
731
|
+
// Not the email. See above.
|
|
732
|
+
accountId: id,
|
|
733
|
+
providerId: "credential",
|
|
734
|
+
userId: id,
|
|
735
|
+
password: await hashPassword(input.password),
|
|
736
|
+
createdAt: now,
|
|
737
|
+
updatedAt: now
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
708
741
|
function defineSeed(seed) {
|
|
709
742
|
return seed;
|
|
710
743
|
}
|
|
@@ -1019,9 +1052,10 @@ function createFake(seed = 20260101) {
|
|
|
1019
1052
|
var fake = createFake();
|
|
1020
1053
|
|
|
1021
1054
|
// src/index.ts
|
|
1022
|
-
var FLARE_VERSION = "0.
|
|
1055
|
+
var FLARE_VERSION = "0.10.0";
|
|
1023
1056
|
export {
|
|
1024
1057
|
DEFAULT_FILE_MAX_BYTES,
|
|
1058
|
+
DEFAULT_MONEY_EXPONENT,
|
|
1025
1059
|
FILE_CATEGORIES,
|
|
1026
1060
|
FLARE_VERSION,
|
|
1027
1061
|
NUMBER_FORMATS,
|
|
@@ -1047,6 +1081,7 @@ export {
|
|
|
1047
1081
|
createObjectKey,
|
|
1048
1082
|
createStorage,
|
|
1049
1083
|
createValidators,
|
|
1084
|
+
credentialUserRows,
|
|
1050
1085
|
currentPeriodEnd,
|
|
1051
1086
|
defaultFieldLabel,
|
|
1052
1087
|
definePolicy,
|
|
@@ -1061,7 +1096,9 @@ export {
|
|
|
1061
1096
|
formValuesToInput,
|
|
1062
1097
|
formatPhone,
|
|
1063
1098
|
formatValue,
|
|
1099
|
+
fromMinorUnits,
|
|
1064
1100
|
grantsAccess,
|
|
1101
|
+
hasTooManyPlaces,
|
|
1065
1102
|
hashPassword,
|
|
1066
1103
|
humanize,
|
|
1067
1104
|
importSigningKey,
|
|
@@ -1076,6 +1113,7 @@ export {
|
|
|
1076
1113
|
kebabCase,
|
|
1077
1114
|
matchesContentType,
|
|
1078
1115
|
mimeTypesFor,
|
|
1116
|
+
moneyExponent,
|
|
1079
1117
|
normalizeDomain,
|
|
1080
1118
|
optionLabel,
|
|
1081
1119
|
ownershipFilter,
|
|
@@ -1096,6 +1134,7 @@ export {
|
|
|
1096
1134
|
subscriptionPriceId,
|
|
1097
1135
|
subscriptionState,
|
|
1098
1136
|
toE164,
|
|
1137
|
+
toMinorUnits,
|
|
1099
1138
|
toSubscriptionStatus,
|
|
1100
1139
|
verifyPassword,
|
|
1101
1140
|
verifyToken,
|
package/dist/server.js
CHANGED
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
can,
|
|
3
3
|
columnName,
|
|
4
4
|
createValidators,
|
|
5
|
+
moneyExponent,
|
|
5
6
|
storedFields
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-TB53VHXY.js";
|
|
7
8
|
|
|
8
9
|
// ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/entity.js
|
|
9
10
|
var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
|
|
@@ -1783,6 +1784,12 @@ function fieldJsonSchema(def) {
|
|
|
1783
1784
|
const schema = { ...base, type: def.kind === "int" ? "integer" : "number" };
|
|
1784
1785
|
if (def.min !== void 0) schema.minimum = def.min;
|
|
1785
1786
|
if (def.max !== void 0) schema.maximum = def.max;
|
|
1787
|
+
if (def.format === "money") {
|
|
1788
|
+
const places = moneyExponent(def.currency);
|
|
1789
|
+
schema.multipleOf = places === 0 ? 1 : Number(`1e-${places}`);
|
|
1790
|
+
schema.description = base.description ?? `An amount in ${def.currency ?? "the app's currency"}, to ${places} decimal place${places === 1 ? "" : "s"}.`;
|
|
1791
|
+
schema.examples = [places === 0 ? 1500 : 19.99];
|
|
1792
|
+
}
|
|
1786
1793
|
return schema;
|
|
1787
1794
|
}
|
|
1788
1795
|
case "boolean":
|