Skip to content

Releases: forestfuture/d1-record

@forestfuture/d1-record@0.1.1

Choose a tag to compare

Patch Changes

  • #117 2938e67 Thanks @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

Choose a tag to compare

Minor Changes

  • 74a956f Thanks @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 the declare posts: HasMany<typeof Post> lines are gone. Move each association into a field, and drop its declare line.

  • e3674a6 Thanks @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" are false for a boolean, "42" is 42 for an integer, and an ISO-8601 string is a Date. Anything else throws TypeError: "no" for a boolean, "12abc" or 1.7 for an integer, a number for a string, a time without an offset. Before this, "false" was saved as true, 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 throws RecordNotFound. In a condition, a number matches a string attribute as its digits (7 finds "7"), as find does for a text key.

    An override that changes an attribute's type without giving a default now drops the generated default, so the column's own default applies.

  • 02094f5 Thanks @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 your ApplicationRecord's base class by d1-record schema). For a D1 Session, per-request onQuery, 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 })), replacing static scopes = this.defineScopes({...}). They inherit like any static.

  • 0f4848f Thanks @christopherstyles! - onQuery listeners see the bound values of filtered attributes as "[FILTERED]", as Rails' logs do, and so do cast errors. static filterAttributes starts 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.

  • 4f48758 Thanks @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/.

  • dd95b62 Thanks @christopherstyles! - Bulk writes run when called, as in Rails: await posts.deleteAll() and updateAll(…) resolve to the number of rows (on none(), 0 without a query), and insert, insertAll, upsert and upsertAll to D1's meta. For db.batch([...]), use their to… forms (toInsert, toUpdateAll, toDeleteAll, …), as records use toSave()/toDestroy(). Breaking: .run() is gone. Drop it where a write ran on its own, and use the to… form where a write went into a batch.

Patch Changes

  • 3927383 Thanks @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 own onQuery. 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, and Server-Timing. Every QueryEvent now has a required model field. A listener that throws no longer fails the query: it's logged with console.error, and the query's result or error reaches the caller unchanged.

  • c510136 Thanks @christopherstyles! - npx d1-record console opens your local D1 database in a console with your Models loaded: const user = await User.find(id), await user.update({ name: "Johnny" }), then user to see it. It takes --database, --models, and --config, keeps its history in .wrangler/d1-record-history (--no-history turns that off), leaves with quit, exit, or Ctrl-D, reads edited Models again with reload! (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; --yes skips that; its prompt says remote> and it keeps no history unless --history), and reads TypeScript Models (extensionless imports and tsconfig.json paths included) through jiti, now a dependency of the command line. A record's errors print their failures (Errors [ { attribute, type, message } ]) instead of Errors {}. The guide has a new Console page.

  • 7e6b502 Thanks @christopherstyles! - The package ships its guide for coding agents, matching its version (llms-full.txt), and an agent skill (SKILL.md, installable with npx skills add forestfuture/d1-record or npx skills experimental_sync). The documentation site publishes llms.txt, llms-full.txt and every page as Markdown.

  • 8a4b6e1 Thanks @christopherstyles! - A misspelled key in an attribute spec ({ type: "string", nul: false }), in Model({...}) or ApplicationRecord({...}), is now a compile error instead of being silently ignored.

  • 6112dfb Thanks @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 with generateId: true, where a default would run (at build(), and in bulk inserts); it's crypto.randomUUID() by default, and gets the attribute's name. d1-record schema now writes generateId: true for a TEXT primary key instead of default: () => crypto.randomUUID(), and its format changed, so run d1-record schema again: --check reports the old file as stale. A Model can turn it on or off with an override ({ publicId: { generateId: true } }). The generated application-record.ts shows a commented-out example.

  • 31c78d3 Thanks @christopherstyles! - d1-record g model and g migration take --out, naming the schema file to regenerate, as d1-record schema does: in a Drizzle project with its own src/db/schema/, --out src/db/d1-record.ts.

  • cd69c3e Thanks @christopherstyles! - Generators: d1-record g model Product name:string! user:references writes a CREATE TABLE migration and src/models/product.ts, and d1-record g migration AddSkuToProducts sku:string:unique writes a migration from its name (Create…, Add…To…, Remove…From…, or empty). Columns take ! for NOT NULL, =value for a literal default, and :unique or :index. Each migration is checked against the existing ones before anything is written, numbered as Wrangler numbers them, and the schema is regenerated afterwards.

  • 286bdea Thanks @christopherstyles! - static hiddenAttributes lists attribute names toJSON() leaves out, so Response.json(user) can't send a passwordDigest. It starts empty on Model.Base; set it on your app's base class (static override hiddenAttributes = ["passwordDigest"]). A Model's own list replaces the base class's, and attributes() still returns everything. A name a Model doesn't have is an InvalidModel when it's first used.

  • [`fb...

Read more