Repository navigation
Releases: forestfuture/d1-record
Release list
@forestfuture/d1-record@0.1.1
Patch Changes
- #117
2938e67Thanks @christopherstyles! - A very long attribute name no longer takes time that grows with the square of its length.record.errors.fullMessages()turned an attribute name into words with a rule that took about 6 seconds for a 100,000-character name, so an app that adds an error under a submitted key could be slowed by a long one. It now takes about a millisecond, and every name comes out as before.
@forestfuture/d1-record@0.1.0
Minor Changes
-
74a956fThanks @christopherstyles! - Associations are declared as record fields, written once:posts = hasMany(Post, { dependent: "destroy" }),author = belongsTo(User),profile = hasOne(Profile), imported from the package. TypeScript infers each accessor's type, and association scopes are typed from their target. Breaking:static associations = this.defineAssociations({...})and thedeclare posts: HasMany<typeof Post>lines are gone. Move each association into a field, and drop itsdeclareline. -
e3674a6Thanks @christopherstyles! - Attribute values are cast to their type when they're assigned or given to a bulk write, and in conditions. Only unambiguous values are accepted:"false","0"and"off"arefalsefor a boolean,"42"is42for an integer, and an ISO-8601 string is aDate. Anything else throwsTypeError:"no"for a boolean,"12abc"or1.7for an integer, a number for a string, a time without an offset. Before this,"false"was saved astrue, and"12abc"was stored in an integer column.A condition value that can't be cast matches nothing, so
find("abc")on an integer key throwsRecordNotFound. In a condition, a number matches a string attribute as its digits (7finds"7"), asfinddoes for a text key.An override that changes an attribute's
typewithout giving adefaultnow drops the generated default, so the column's own default applies. -
02094f5Thanks @christopherstyles! - Queries start from the Model, as in Rails:User.find(id),User.where(…),User.create(…),User.active(), and every other Relation method as a typed static. With no setup they use the Model's binding (static binding = "DB", written on yourApplicationRecord's base class byd1-record schema). For a D1 Session, per-requestonQuery, or tests outside a Worker,withDatabase(connect(env.DB, { … }), fn)gives a request its own database.batch([...])writes atomically on the current database.db.model(X)still works as before.Scopes are now declared one static each, as Rails'
scope :active, -> { … }:static active = this.scope((q) => q.where({ isActive: true })), replacingstatic scopes = this.defineScopes({...}). They inherit like any static. -
0f4848fThanks @christopherstyles! -onQuerylisteners see the bound values of filtered attributes as"[FILTERED]", as Rails' logs do, and so do cast errors.static filterAttributesstarts with Rails' default list (passw,email,secret,token,_key,crypt,salt,certificate,otp,ssn,cvv,cvc), matched against each attribute's snake_case name, so passwords, tokens, API keys and emails no longer reach your logs. Replace the list on your app's base class:static override filterAttributes = [...Model.Base.filterAttributes, "iban"], or[]to filter nothing. D1 still gets the real values. -
4f48758Thanks @christopherstyles! - First release: a Rails-inspired Active Record for Cloudflare Workers and D1. Models with typed attributes, validations, callbacks, associations and change tracking; lazy, composable queries; atomic batches; and read-replica sessions. See the guide at https://docs.forestfuture.dev/d1-record/. -
dd95b62Thanks @christopherstyles! - Bulk writes run when called, as in Rails:await posts.deleteAll()andupdateAll(…)resolve to the number of rows (onnone(),0without a query), andinsert,insertAll,upsertandupsertAllto D1'smeta. Fordb.batch([...]), use theirto…forms (toInsert,toUpdateAll,toDeleteAll, …), as records usetoSave()/toDestroy(). Breaking:.run()is gone. Drop it where a write ran on its own, and use theto…form where a write went into a batch.
Patch Changes
-
3927383Thanks @christopherstyles! - An app-wide query logger:static override onQuery: QueryListener = (event) => …on your app's base class sees every statement of every Model, whichever database runs it, after the database's ownonQuery. Every query event now says which Model it's for (event.model). The guide's Connecting page has a new Logging queries section: app-wide, per request with its ID, development only, Workers Logs, andServer-Timing. EveryQueryEventnow has a requiredmodelfield. A listener that throws no longer fails the query: it's logged withconsole.error, and the query's result or error reaches the caller unchanged. -
c510136Thanks @christopherstyles! -npx d1-record consoleopens your local D1 database in a console with your Models loaded:const user = await User.find(id),await user.update({ name: "Johnny" }), thenuserto see it. It takes--database,--models, and--config, keeps its history in.wrangler/d1-record-history(--no-historyturns that off), leaves withquit,exit, or Ctrl-D, reads edited Models again withreload!(or.reload), runs one piece of code with-e(its result alone on stdout, a failure on stderr with exit code 1), shows the SQL of each line with--verbose(or.verbose,verbose!while it runs), opens your remote database only with--remote(after you type its name;--yesskips that; its prompt saysremote>and it keeps no history unless--history), and reads TypeScript Models (extensionless imports andtsconfig.jsonpaths included) throughjiti, now a dependency of the command line. A record'serrorsprint their failures (Errors [ { attribute, type, message } ]) instead ofErrors {}. The guide has a new Console page. -
7e6b502Thanks @christopherstyles! - The package ships its guide for coding agents, matching its version (llms-full.txt), and an agent skill (SKILL.md, installable withnpx skills add forestfuture/d1-recordornpx skills experimental_sync). The documentation site publishesllms.txt,llms-full.txtand every page as Markdown. -
8a4b6e1Thanks @christopherstyles! - A misspelled key in an attribute spec ({ type: "string", nul: false }), inModel({...})orApplicationRecord({...}), is now a compile error instead of being silently ignored. -
6112dfbThanks @christopherstyles! - Generated ids: choose how your app's keys are made in one place.static override generateId = () => uuidv7()on your app's base class makes every attribute declared withgenerateId: true, where a default would run (atbuild(), and in bulk inserts); it'scrypto.randomUUID()by default, and gets the attribute's name.d1-record schemanow writesgenerateId: truefor aTEXTprimary key instead ofdefault: () => crypto.randomUUID(), and its format changed, so rund1-record schemaagain:--checkreports the old file as stale. A Model can turn it on or off with an override ({ publicId: { generateId: true } }). The generatedapplication-record.tsshows a commented-out example. -
31c78d3Thanks @christopherstyles! -d1-record g modelandg migrationtake--out, naming the schema file to regenerate, asd1-record schemadoes: in a Drizzle project with its ownsrc/db/schema/,--out src/db/d1-record.ts. -
cd69c3eThanks @christopherstyles! - Generators:d1-record g model Product name:string! user:referenceswrites aCREATE TABLEmigration andsrc/models/product.ts, andd1-record g migration AddSkuToProducts sku:string:uniquewrites a migration from its name (Create…,Add…To…,Remove…From…, or empty). Columns take!forNOT NULL,=valuefor a literal default, and:uniqueor:index. Each migration is checked against the existing ones before anything is written, numbered as Wrangler numbers them, and the schema is regenerated afterwards. -
286bdeaThanks @christopherstyles! -static hiddenAttributeslists attribute namestoJSON()leaves out, soResponse.json(user)can't send apasswordDigest. It starts empty onModel.Base; set it on your app's base class (static override hiddenAttributes = ["passwordDigest"]). A Model's own list replaces the base class's, andattributes()still returns everything. A name a Model doesn't have is anInvalidModelwhen it's first used. -
[`fb...