github
html_url | issue_url | id | node_id | user | created_at | updated_at | author_association | body | reactions | issue | performed_via_github_app |
---|---|---|---|---|---|---|---|---|---|---|---|
https://github.com/simonw/sqlite-utils/issues/215#issuecomment-753661292 | https://api.github.com/repos/simonw/sqlite-utils/issues/215 | 753661292 | MDEyOklzc3VlQ29tbWVudDc1MzY2MTI5Mg== | 9599 | 2021-01-03T18:56:06Z | 2021-01-03T18:56:23Z | OWNER | Another option: on creation of the `Database()` object, check to see if the `_counts` table exists and use that as the default for a `use_counts_table` property. Also flip that property to `True` if the user calls `.enable_counts()` at any time. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777535402 | |
https://github.com/simonw/sqlite-utils/issues/215#issuecomment-753661158 | https://api.github.com/repos/simonw/sqlite-utils/issues/215 | 753661158 | MDEyOklzc3VlQ29tbWVudDc1MzY2MTE1OA== | 9599 | 2021-01-03T18:55:16Z | 2021-01-03T18:55:16Z | OWNER | Alternative implementation: provided `db.should_trust_counts` is `True`, try running the query: ```sql select count from _counts where [table] = ? ``` If the query fails to return a result OR throws an error because the table doesn't exist, run the `count(*)` query. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777535402 | |
https://github.com/simonw/sqlite-utils/issues/215#issuecomment-753660814 | https://api.github.com/repos/simonw/sqlite-utils/issues/215 | 753660814 | MDEyOklzc3VlQ29tbWVudDc1MzY2MDgxNA== | 9599 | 2021-01-03T18:53:05Z | 2021-01-03T18:53:05Z | OWNER | Here's the current `.count` property: https://github.com/simonw/sqlite-utils/blob/036ec6d32313487527c66dea613a3e7118b97459/sqlite_utils/db.py#L597-L609 It's implemented on `Queryable` which means it's available on both `Table` and `View` - the optimization doesn't make sense for views. I'm a bit cautious about making that property so much more complex. In order to decide if it should try the `_counts` table first it needs to know: - Should it be trusting the counts? I'm thinking a `.should_trust_counts` property on `Database` which defaults to `True` would be good - then advanced users can turn that off if they know the counts should not be trusted. - Does the `_counts` table exist? - Are the triggers defined? Then it can do the query, and if the query fails it can fall back on the `count(*)`. That's quite a lot of extra activity though. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777535402 | |
https://github.com/simonw/sqlite-utils/issues/215#issuecomment-753660379 | https://api.github.com/repos/simonw/sqlite-utils/issues/215 | 753660379 | MDEyOklzc3VlQ29tbWVudDc1MzY2MDM3OQ== | 9599 | 2021-01-03T18:50:15Z | 2021-01-03T18:50:15Z | OWNER | ```python def cached_counts(self, tables=None): sql = "select [table], count from {}".format(self._counts_table_name) if tables: sql += " where [table] in ({})".format(", ".join("?" for table in tables)) return {r[0]: r[1] for r in self.execute(sql, tables).fetchall()} ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777535402 | |
https://github.com/simonw/sqlite-utils/issues/206#issuecomment-753659260 | https://api.github.com/repos/simonw/sqlite-utils/issues/206 | 753659260 | MDEyOklzc3VlQ29tbWVudDc1MzY1OTI2MA== | 9599 | 2021-01-03T18:42:01Z | 2021-01-03T18:42:01Z | OWNER | ``` % sqlite-utils insert blah.db blah global_power_plant_database.csv Error: Invalid JSON - use --csv for CSV or --tsv for TSV files ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
761915790 | |
https://github.com/simonw/datasette/issues/1169#issuecomment-753657180 | https://api.github.com/repos/simonw/datasette/issues/1169 | 753657180 | MDEyOklzc3VlQ29tbWVudDc1MzY1NzE4MA== | 9599 | 2021-01-03T18:23:30Z | 2021-01-03T18:23:30Z | OWNER | Also welcome in that PR would be a bit of documentation for contributors, see #1167 - but no problem if you leave that out, I'm happy to add it later. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777677671 | |
https://github.com/simonw/datasette/issues/1169#issuecomment-753653260 | https://api.github.com/repos/simonw/datasette/issues/1169 | 753653260 | MDEyOklzc3VlQ29tbWVudDc1MzY1MzI2MA== | 9599 | 2021-01-03T17:54:40Z | 2021-01-03T17:54:40Z | OWNER | And @benpickles yes I would land that pull request straight away as-is. Thanks! | { "total_count": 1, "+1": 1, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777677671 | |
https://github.com/simonw/datasette/issues/1169#issuecomment-753653033 | https://api.github.com/repos/simonw/datasette/issues/1169 | 753653033 | MDEyOklzc3VlQ29tbWVudDc1MzY1MzAzMw== | 9599 | 2021-01-03T17:52:53Z | 2021-01-03T17:52:53Z | OWNER | Oh that's so frustrating! I was worried about that - I spotted a few runs that seemed faster and hoped that it meant that the package was coming out of the `~/.npm` cache, but evidently that's not the case. You've convinced me that Datasette itself should have a `package.json` - the Dependabot argument is a really good one. But... I'd really love to figure out a general pattern for using `npx` scripts in GitHub Actions workflows in a cache-friendly way. I have plenty of other projects that I'd love to run Prettier or Uglify or `puppeteer-cli` in without adding a `package.json` to them. Any ideas? The best I can think of is for the workflow itself to write out a `package.json` file (using `echo '{ ... }' > package.json`) as part of the run - that way the cache should work (I think) but I don't get a misleading `package.json` file sitting in the repo. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777677671 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753570710 | https://api.github.com/repos/simonw/datasette/issues/983 | 753570710 | MDEyOklzc3VlQ29tbWVudDc1MzU3MDcxMA== | 9599 | 2021-01-03T05:29:56Z | 2021-01-03T05:29:56Z | OWNER | I thought about using browser events, but they don't quite match the API that I'm looking to provide. In particular, the great thing about Pluggy is that if you have multiple handlers registered for a specific plugin hook each of those handlers can return a value, and Pluggy will combine those values into a list of replies. This is great for things like plugin hooks that add extra menu items - each plugin can return a menu item (maybe as a label/URL/click-callback object) and the calling code can then add all of those items to the menu. See https://docs.datasette.io/en/stable/plugin_hooks.html#table-actions-datasette-actor-database-table for a Python example. I'm on the fence about relying on JavaScript modules. I need to think about browser compatibility for them - but I'm already commited to requiring support for `() => {}` arrow functions so maybe I'm committed to module support too already? | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/1160#issuecomment-753568428 | https://api.github.com/repos/simonw/datasette/issues/1160 | 753568428 | MDEyOklzc3VlQ29tbWVudDc1MzU2ODQyOA== | 9599 | 2021-01-03T05:02:32Z | 2021-01-03T05:02:32Z | OWNER | Should this command include a `--fts` option for configuring full-text search on one-or-more columns? I thought about doing that for `sqlite-utils insert` in https://github.com/simonw/sqlite-utils/issues/202 and decided not to because of the need to include extra options covering the FTS version, porter stemming options and whether or not to create triggers. But maybe I can set sensible defaults for that with `datasette insert ... -f title -f body`? Worth thinking about a bit more. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
775666296 | |
https://github.com/simonw/sqlite-utils/issues/202#issuecomment-753568264 | https://api.github.com/repos/simonw/sqlite-utils/issues/202 | 753568264 | MDEyOklzc3VlQ29tbWVudDc1MzU2ODI2NA== | 9599 | 2021-01-03T05:00:24Z | 2021-01-03T05:00:24Z | OWNER | I'm not going to implement this, because it actually needs several additional options that already exist on `sqlite-utils enable-fts`: ``` --fts4 Use FTS4 --fts5 Use FTS5 --tokenize TEXT Tokenizer to use, e.g. porter --create-triggers Create triggers to update the FTS tables when the parent table changes. ``` I'd rather not add all four of those options to `sqlite-utils insert` just to support this shortcut. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
738514367 | |
https://github.com/simonw/sqlite-utils/issues/202#issuecomment-753567969 | https://api.github.com/repos/simonw/sqlite-utils/issues/202 | 753567969 | MDEyOklzc3VlQ29tbWVudDc1MzU2Nzk2OQ== | 9599 | 2021-01-03T04:55:17Z | 2021-01-03T04:55:43Z | OWNER | The long version of this can be `--fts`, same as in `csvs-to-sqlite`. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
738514367 | |
https://github.com/simonw/sqlite-utils/pull/203#issuecomment-753567932 | https://api.github.com/repos/simonw/sqlite-utils/issues/203 | 753567932 | MDEyOklzc3VlQ29tbWVudDc1MzU2NzkzMg== | 9599 | 2021-01-03T04:54:43Z | 2021-01-03T04:54:43Z | OWNER | Another option: expand the `ForeignKey` object to have `.columns` and `.other_columns` properties in addition to the existing `.column` and `.other_column` properties. These new plural properties would always return a tuple, which would be a one-item tuple for a non-compound-foreign-key. The question then is what should `.column` and `.other_column` return for compound foreign keys? I'd be inclined to say they should return `None` - which would trigger errors in code that encounters a compound foreign key for the first time, but those errors would at least be a strong indicator as to what had gone wrong. We can label `.column` and `.other_column` as deprecated and then remove them in `sqlite-utils 4.0`. Since this would still be a breaking change in some minor edge-cases I'm thinking maybe 4.0 needs to happen in order to land this feature. I'm not opposed to doing that, I was just hoping it might be avoidable. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
743384829 | |
https://github.com/simonw/sqlite-utils/pull/203#issuecomment-753567744 | https://api.github.com/repos/simonw/sqlite-utils/issues/203 | 753567744 | MDEyOklzc3VlQ29tbWVudDc1MzU2Nzc0NA== | 9599 | 2021-01-03T04:51:44Z | 2021-01-03T04:51:44Z | OWNER | One way that this could avoid a breaking change would be to have `fk.column` and `fk.other_column` remain as strings for non-compound-foreign-keys, but turn into tuples for a compound foreign key. This is a bit of an ugly API design, and it could still break existing code that encounters a compound foreign key for the first time - but it would leave code working for the more common case of a non-compound-foreign-key. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
743384829 | |
https://github.com/simonw/sqlite-utils/pull/203#issuecomment-753567508 | https://api.github.com/repos/simonw/sqlite-utils/issues/203 | 753567508 | MDEyOklzc3VlQ29tbWVudDc1MzU2NzUwOA== | 9599 | 2021-01-03T04:48:17Z | 2021-01-03T04:48:17Z | OWNER | Sorry for taking so long to review this! This approach looks great to me - being able to optionally pass a tuple anywhere the API currently expects a column is smart, and it's consistent with how the `pk=` parameter works elsewhere. There's just one problem I can see with this: the way it changes the `ForeignKey(...)` interface to always return a tuple for `.column` and `.other_column`, even if that tuple only contains a single item. This represents a breaking change to the existing API - any code that expects `ForeignKey.column` to be a single string (which is any code that has been written against that) will break. As such, I'd have to bump the major version of `sqlite-utils` to `4.0` in order to ship this. Ideally I'd like to make this change in a way that doesn't represent an API compatibility break. I need to think a bit harder about how that might be achieved. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
743384829 | |
https://github.com/simonw/sqlite-utils/issues/217#issuecomment-753566184 | https://api.github.com/repos/simonw/sqlite-utils/issues/217 | 753566184 | MDEyOklzc3VlQ29tbWVudDc1MzU2NjE4NA== | 9599 | 2021-01-03T04:27:38Z | 2021-01-03T04:27:38Z | OWNER | Documented here: https://sqlite-utils.datasette.io/en/latest/python-api.html#quoting-strings-for-use-in-sql | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777543336 | |
https://github.com/simonw/sqlite-utils/issues/216#issuecomment-753566156 | https://api.github.com/repos/simonw/sqlite-utils/issues/216 | 753566156 | MDEyOklzc3VlQ29tbWVudDc1MzU2NjE1Ng== | 9599 | 2021-01-03T04:27:14Z | 2021-01-03T04:27:14Z | OWNER | Documented here: https://sqlite-utils.datasette.io/en/latest/python-api.html#introspection | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777540352 | |
https://github.com/simonw/sqlite-utils/issues/218#issuecomment-753563757 | https://api.github.com/repos/simonw/sqlite-utils/issues/218 | 753563757 | MDEyOklzc3VlQ29tbWVudDc1MzU2Mzc1Nw== | 9599 | 2021-01-03T03:49:51Z | 2021-01-03T03:49:51Z | OWNER | Documentation: https://sqlite-utils.datasette.io/en/latest/cli.html#listing-triggers | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777560474 | |
https://github.com/simonw/sqlite-utils/issues/215#issuecomment-753545757 | https://api.github.com/repos/simonw/sqlite-utils/issues/215 | 753545757 | MDEyOklzc3VlQ29tbWVudDc1MzU0NTc1Nw== | 9599 | 2021-01-02T23:58:07Z | 2021-01-02T23:58:07Z | OWNER | Thought: maybe there should be a `.reset_counts()` method too, for if the table gets out of date with the triggers. One way that could happen is if a table is dropped and recreated - the counts in the `_counts` table would likely no longer match the number of rows in that table. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777535402 | |
https://github.com/simonw/sqlite-utils/issues/215#issuecomment-753545381 | https://api.github.com/repos/simonw/sqlite-utils/issues/215 | 753545381 | MDEyOklzc3VlQ29tbWVudDc1MzU0NTM4MQ== | 9599 | 2021-01-02T23:52:52Z | 2021-01-02T23:52:52Z | OWNER | Idea: a `db.cached_counts()` method that returns a dictionary of data from the `_counts` table. Call it with a list of tables to get back the counts for just those tables. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777535402 | |
https://github.com/simonw/sqlite-utils/issues/217#issuecomment-753544914 | https://api.github.com/repos/simonw/sqlite-utils/issues/217 | 753544914 | MDEyOklzc3VlQ29tbWVudDc1MzU0NDkxNA== | 9599 | 2021-01-02T23:47:42Z | 2021-01-02T23:47:42Z | OWNER | https://github.com/simonw/sqlite-utils/blob/9a5c92b63e7917c93cc502478493c51c781b2ecc/sqlite_utils/db.py#L231-L239 | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777543336 | |
https://github.com/simonw/sqlite-utils/issues/213#issuecomment-753535488 | https://api.github.com/repos/simonw/sqlite-utils/issues/213 | 753535488 | MDEyOklzc3VlQ29tbWVudDc1MzUzNTQ4OA== | 9599 | 2021-01-02T22:03:48Z | 2021-01-02T22:03:48Z | OWNER | I got this error while prototyping this: too many levels of trigger recursion It looks like that's because SQLite doesn't like triggers on a table that themselves then update that table - so I'm going to exclude the `_counts` table from this mechanism. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777529979 | |
https://github.com/simonw/sqlite-utils/issues/213#issuecomment-753533775 | https://api.github.com/repos/simonw/sqlite-utils/issues/213 | 753533775 | MDEyOklzc3VlQ29tbWVudDc1MzUzMzc3NQ== | 9599 | 2021-01-02T21:47:10Z | 2021-01-02T21:47:10Z | OWNER | I'm going to skip virtual tables, which I can identify using this property: https://github.com/simonw/sqlite-utils/blob/1cad7fad3e7a5b734088f5cc545b69a055e636da/sqlite_utils/db.py#L720-L726 | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777529979 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753524779 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753524779 | MDEyOklzc3VlQ29tbWVudDc1MzUyNDc3OQ== | 9599 | 2021-01-02T20:19:26Z | 2021-01-02T20:19:26Z | OWNER | Idea: version the metadata scheme. If the table is called `_metadata_v1` it gives me a clear path to designing a new scheme in the future. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/sqlite-utils/issues/212#issuecomment-753422324 | https://api.github.com/repos/simonw/sqlite-utils/issues/212 | 753422324 | MDEyOklzc3VlQ29tbWVudDc1MzQyMjMyNA== | 9599 | 2021-01-02T03:00:34Z | 2021-01-02T03:00:34Z | OWNER | Here's a prototype: ```python with db.conn: db.conn.executescript(""" CREATE TABLE IF NOT EXISTS [_counts] ([table] TEXT PRIMARY KEY, [count] INTEGER DEFAULT 0); CREATE TRIGGER IF NOT EXISTS [Street_Tree_List_counts_ai] AFTER INSERT ON [Street_Tree_List] BEGIN INSERT OR REPLACE INTO _counts VALUES ('Street_Tree_List', COALESCE( (SELECT count FROM _counts WHERE [table]='Street_Tree_List'), 0) + 1); END; CREATE TRIGGER IF NOT EXISTS [Street_Tree_List_counts_ad] AFTER DELETE ON [Street_Tree_List] BEGIN INSERT OR REPLACE INTO _counts VALUES ('Street_Tree_List', COALESCE( (SELECT count FROM _counts WHERE [table]='Street_Tree_List'), 0) - 1); END; INSERT OR REPLACE INTO _counts VALUES ('Street_Tree_List', (select count(*) from [Street_Tree_List])); """) ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777392020 | |
https://github.com/simonw/sqlite-utils/issues/210#issuecomment-753406744 | https://api.github.com/repos/simonw/sqlite-utils/issues/210 | 753406744 | MDEyOklzc3VlQ29tbWVudDc1MzQwNjc0NA== | 9599 | 2021-01-02T00:02:39Z | 2021-01-02T00:02:39Z | OWNER | It looks like https://github.com/ofajardo/pyreadr is a good library for this. I won't add this to `sqlite-utils` because it's quite a bulky dependency for a relatively small feature. Normally I'd write a `rdata-to-sqlite` tool similar to https://pypi.org/project/dbf-to-sqlite/ - but I'm actually working on a new plugin hook for Datasette that might be an even better fit for this. The idea is to allow Datasette plugins to define input formats - such as RData - which would then result in being able to import them on the command-line with `datasette insert my.db file.rdata` or by uploading a file through the Datasette web interface. That work is happening over here: https://github.com/simonw/datasette/issues/1160 - I'll close this issue in favour of a sometime-in-the-future `datasette-import-rdata` plugin. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
767685961 | |
https://github.com/simonw/sqlite-utils/issues/209#issuecomment-753405835 | https://api.github.com/repos/simonw/sqlite-utils/issues/209 | 753405835 | MDEyOklzc3VlQ29tbWVudDc1MzQwNTgzNQ== | 9599 | 2021-01-01T23:52:06Z | 2021-01-01T23:52:06Z | OWNER | I just hit this one too. Such a weird bug! | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
766156875 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753402423 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753402423 | MDEyOklzc3VlQ29tbWVudDc1MzQwMjQyMw== | 9599 | 2021-01-01T23:16:05Z | 2021-01-01T23:16:05Z | OWNER | One catch: solving the "show me all metadata for everything in this Datasette instance" problem. Ideally there would be a SQLite table that can be queried for this. But the need to resolve the potentially complex set of precedence rules means that table would be difficult if not impossible to provide at run-time. Ideally a denormalized table would be available that featured the results of running those precedence rule calculations. But how to handle keeping this up-to-date? It would need to be recalculated any time a `_metadata` table in any of the attached databases had an update. This is a much larger problem - but one potential fix would be to use triggers to maintain a "version number" for the `_metadata` table - similar to SQLite's own built-in `schema_version` mechanism. Triggers could increment a counter any time a record in that table was added, deleted or updated. Such a mechanism would have applications outside of just this `_metadata` system. The ability to attach a version number to any table and have it automatically incremented when that table changes (via triggers) could help with all kinds of other Datasette-at-scale problems, including things like cached table counts. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753401001 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753401001 | MDEyOklzc3VlQ29tbWVudDc1MzQwMTAwMQ== | 9599 | 2021-01-01T23:01:45Z | 2021-01-01T23:01:45Z | OWNER | I need to prototype this. Could I do that as a plugin? I think so - I could try out the algorithm for loading metadata and display it on pages using some custom templates. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753400420 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753400420 | MDEyOklzc3VlQ29tbWVudDc1MzQwMDQyMA== | 9599 | 2021-01-01T22:53:58Z | 2021-01-01T22:53:58Z | OWNER | Precedence idea: - First priority is non-_internal metadata from other databases - if those conflict then pick then the alphabetically-ordered-first database name wins - Next priority: `_internal` metadata, which should have been loaded from `metadata.json` - Last priority: the `_metadata` table from that database itself, i.e. the default "baked in" metadata | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753400306 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753400306 | MDEyOklzc3VlQ29tbWVudDc1MzQwMDMwNg== | 9599 | 2021-01-01T22:52:44Z | 2021-01-01T22:52:44Z | OWNER | Also: probably load column metadata as part of the table metadata rather than loading column metadata individually, since it's going to be rare to want the metadata for a single column rather than for an entire table full of columns. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753400265 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753400265 | MDEyOklzc3VlQ29tbWVudDc1MzQwMDI2NQ== | 9599 | 2021-01-01T22:52:09Z | 2021-01-01T22:52:09Z | OWNER | From an implementation perspective, I think the way this works is SQL queries read the relevant metadata from ALL available metadata tables, then Python code solves the precedence rules to produce the final, combined metadata for a database/table/column. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753399635 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753399635 | MDEyOklzc3VlQ29tbWVudDc1MzM5OTYzNQ== | 9599 | 2021-01-01T22:45:21Z | 2021-01-01T22:50:21Z | OWNER | Would also need to figure out the precedence rules: - What happens if the database has a `_metadata` table with data that conflicts with a remote metadata record from another database? I think the other database should win, because that allows plugins to over-ride the default metadata for something. - Do JSON values get merged together? So if one table provides a description and another provides a title do both values get returned? - If a database has a `license`, does that "cascade" down to the tables? What about `source` and `about`? - What if there are two databases (or more) that provide conflicting metadata for a table in some other database? Also, `_internal` may have loaded data from `metadata.json` that conflicts with some other remote table metadata definition. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753399428 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753399428 | MDEyOklzc3VlQ29tbWVudDc1MzM5OTQyOA== | 9599 | 2021-01-01T22:43:14Z | 2021-01-01T22:43:22Z | OWNER | Could this use a compound primary key on `database, table, column`? Does that work with null values? | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753399366 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753399366 | MDEyOklzc3VlQ29tbWVudDc1MzM5OTM2Ng== | 9599 | 2021-01-01T22:42:37Z | 2021-01-01T22:42:37Z | OWNER | So what would the database schema for this look like? I'm leaning towards a single table called `_metadata`, because that's a neater fit for baking the metadata into the database file along with the data that it is describing. Alternatively I could have multiple tables sharing that prefix - `_metadata_database` and `_metadata_tables` and `_metadata_columns` perhaps. If it's just a single `_metadata` table, the schema could look like this: | database | table | column | metadata | | --- | --- | --- | --- | | | mytable | | {"title": "My Table" } | | | mytable | mycolumn | {"description": "Column description" } | | otherdb | othertable | | {"description": "Table in another DB" } | If the `database` column is `null` it means "this is describing a table in the same database file as this `_metadata` table". The alternative to the `metadata` JSON column would be separate columns for each potential metadata value - `license`, `source`, `about`, `about_url` etc. But that makes it harder for people to create custom metadata fields. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753398542 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753398542 | MDEyOklzc3VlQ29tbWVudDc1MzM5ODU0Mg== | 9599 | 2021-01-01T22:37:24Z | 2021-01-01T22:37:24Z | OWNER | The direction I'm leaning in now is the following: - Metadata always lives in SQLite tables - These tables can be co-located with the database they describe (same DB file) - ... or they can be in a different DB file and reference the other database that they are describing - Metadata provided on startup in a `metadata.json` file is loaded into an in-memory metadata table using that same mechanism Plugins that want to provide metadata can do so by populating a table. They could even maintain their own in-memory database for this, or they could write to the `_internal` in-memory database, or they could write to a table in a database on disk. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753392102 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753392102 | MDEyOklzc3VlQ29tbWVudDc1MzM5MjEwMg== | 9599 | 2021-01-01T22:06:33Z | 2021-01-01T22:06:33Z | OWNER | Some SQLite databases include SQL comments in the schema definition which tell you what each column means: ```sql CREATE TABLE User -- A table comment ( uid INTEGER, -- A field comment flags INTEGER -- Another field comment ); ``` The problem with these is that they're not exposed to SQLite in any mechanism other than parsing the `CREATE TABLE` statement from the `sqlite_master` table to extract those columns. I had an idea to build a plugin that could return these. That would be easy with a "get metadata for this column" plugin hook - in the absence of one a plugin could still run that reads the schemas on startup and uses them to populate a metadata database table somewhere. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753391869 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753391869 | MDEyOklzc3VlQ29tbWVudDc1MzM5MTg2OQ== | 9599 | 2021-01-01T22:04:30Z | 2021-01-01T22:04:30Z | OWNER | The sticking point here seems to be the plugin hook. Allowing plugins to over-ride the way the question "give me the metadata for this database/table/column" is answered makes the database-backed metadata mechanisms much more complicated to think about. What if plugins didn't get to over-ride metadata in this way, but could instead update the metadata in a persistent Datasette-managed storage mechanism? Then maybe Datasette could do the following: - Maintain metadata in `_internal` that has been loaded from `metadata.json` - Know how to check a database for baked-in metadata (maybe in a `_metadata` table) - Know how to fall back on the `_internal` metadata if no baked-in metadata is available If database files were optionally allowed to store metadata about tables that live in another database file this could perhaps solve the plugin needs - since an "edit metadata" plugin would be able to edit records in a separate, dedicated `metadata.db` database to store new information about tables in other files. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753390791 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753390791 | MDEyOklzc3VlQ29tbWVudDc1MzM5MDc5MQ== | 9599 | 2021-01-01T22:00:42Z | 2021-01-01T22:00:42Z | OWNER | Here are the requirements I'm currently trying to satisfy: - It should be possible to query the metadata for ALL attached tables in one place, potentially with pagination and filtering - Metadata should be able to exist in the current `metadata.json` file - It should also be possible to bundle metadata in a table in the SQLite database files themselves - Plugins should be able to define their own special mechanisms for metadata. This is particularly interesting for providing a UI that allows users to edit the metadata for their existing tables. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753390262 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753390262 | MDEyOklzc3VlQ29tbWVudDc1MzM5MDI2Mg== | 9599 | 2021-01-01T21:58:11Z | 2021-01-01T21:58:11Z | OWNER | One possibility: plugins could write directly to that in-memory database table. But how would they know to write again should the server restart? Maybe they would write to it once when called by the `startup` plugin hook, and then update it (and their own backing store) when metadata changes for some reason. Feels a bit messy though. Also: if I want to support metadata optionally living in a `_metadata` table colocated with the data in a SQLite database file itself, how would that affect the `metadata` columns in `_internal`? How often would Datasette denormalize and copy data across from the on-disk `_metadata` tables to the `_internal` in-memory columns? | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753389938 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753389938 | MDEyOklzc3VlQ29tbWVudDc1MzM4OTkzOA== | 9599 | 2021-01-01T21:54:15Z | 2021-01-01T21:54:15Z | OWNER | So what if the `databases`, `tables` and `columns` tables in `_internal` each grew a new `metadata` text column? These columns could be populated by Datasette on startup through reading the `metadata.json` file. But how would plugins interact with them? | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753389477 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753389477 | MDEyOklzc3VlQ29tbWVudDc1MzM4OTQ3Nw== | 9599 | 2021-01-01T21:49:57Z | 2021-01-01T21:49:57Z | OWNER | What if metadata was stored in a JSON text column in the existing `_internal` tables? This would allow for users to invent additional metadata fields in the future beyond the current `license`, `license_url` etc fields - without needing a schema change. The downside of JSON columns generally is that they're harder to run indexed queries against. For metadata I don't think that matters - even with 10,000 tables each with their own metadata a SQL query asking for e.g. "everything that has Apache 2 as the license" would return in just a few ms. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753388809 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753388809 | MDEyOklzc3VlQ29tbWVudDc1MzM4ODgwOQ== | 9599 | 2021-01-01T21:47:51Z | 2021-01-01T21:47:51Z | OWNER | A database that exposes metadata will have the same restriction as the new `_internal` database that exposes columns and tables, in that it needs to take permissions into account. A user should not be able to view metadata for tables that they are not able to see. As such, I'd rather bundle any metadata tables into the existing `_internal` database so I don't have to solve that permissions problem in two places. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1168#issuecomment-753366024 | https://api.github.com/repos/simonw/datasette/issues/1168 | 753366024 | MDEyOklzc3VlQ29tbWVudDc1MzM2NjAyNA== | 9599 | 2021-01-01T18:48:34Z | 2021-01-01T18:48:34Z | OWNER | Also: in #188 I proposed bundling metadata in the SQLite database itself alongside the data. This is a great way of ensuring metadata travels with the data when it is downloaded as a SQLite `.db` file. But how would that play with the idea of an in-memory `_metadata` table? Could that table perhaps offer views that join data across multiple attached physical databases? | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777333388 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753224351 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753224351 | MDEyOklzc3VlQ29tbWVudDc1MzIyNDM1MQ== | 9599 | 2020-12-31T23:23:29Z | 2020-12-31T23:23:29Z | OWNER | I should configure the action to only run if changes have been made within the `datasette/static` directory. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753221646 | https://api.github.com/repos/simonw/datasette/issues/983 | 753221646 | MDEyOklzc3VlQ29tbWVudDc1MzIyMTY0Ng== | 9599 | 2020-12-31T22:58:47Z | 2020-12-31T22:58:47Z | OWNER | https://github.com/mishoo/UglifyJS/issues/1905#issuecomment-300485490 says: > `sourceMappingURL` aren't added by default in `3.x` due to one of the feature requests not to - some users are putting them within HTTP response headers instead. > > So the command line for that would be: > > ```js > $ uglifyjs main.js -cmo main.min.js --source-map url=main.min.js.map > ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-753221362 | https://api.github.com/repos/simonw/datasette/issues/1164 | 753221362 | MDEyOklzc3VlQ29tbWVudDc1MzIyMTM2Mg== | 9599 | 2020-12-31T22:55:57Z | 2020-12-31T22:55:57Z | OWNER | I had to add this as the first line in `table.min.js` for the source mapping to work: ``` //# sourceMappingURL=/-/static/table.min.js.map ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-753220665 | https://api.github.com/repos/simonw/datasette/issues/1164 | 753220665 | MDEyOklzc3VlQ29tbWVudDc1MzIyMDY2NQ== | 9599 | 2020-12-31T22:49:36Z | 2020-12-31T22:49:36Z | OWNER | I started with a 7K `table.js` file. `npx uglifyjs table.js --source-map -o table.min.js` gave me a 5.6K `table.min.js` file. `npx uglifyjs table.js --source-map -o table.min.js --compress --mangle` gave me 4.5K. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-753220412 | https://api.github.com/repos/simonw/datasette/issues/1164 | 753220412 | MDEyOklzc3VlQ29tbWVudDc1MzIyMDQxMg== | 9599 | 2020-12-31T22:47:36Z | 2020-12-31T22:47:36Z | OWNER | I'm trying to minify `table.js` and I ran into a problem: Uglification failed. Unexpected character '`' It turns out `uglify-js` doesn't support ES6 syntax! But `uglify-es` does: npm install uglify-es Annoyingly it looks like `uglify-es` uses the same CLI command, `uglifyjs`. So after installing it this seemed to work: npx uglifyjs table.js --source-map -o table.min.js I really don't like how `npx uglifyjs` could mean different things depending on which package was installed. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753219521 | https://api.github.com/repos/simonw/datasette/issues/983 | 753219521 | MDEyOklzc3VlQ29tbWVudDc1MzIxOTUyMQ== | 9599 | 2020-12-31T22:39:52Z | 2020-12-31T22:39:52Z | OWNER | For inlining the `plugins.min.js` file into the Jinja templates I could use the trick described here: https://stackoverflow.com/a/41404611 - which adds a `{{ include_file('file.txt') }}` function to Jinja. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753219407 | https://api.github.com/repos/simonw/datasette/issues/983 | 753219407 | MDEyOklzc3VlQ29tbWVudDc1MzIxOTQwNw== | 9599 | 2020-12-31T22:38:45Z | 2020-12-31T22:39:10Z | OWNER | You'll be able to add JavaScript plugins using a bunch of different mechanisms: - In a custom template, dropping the code in to a `<script>` block - A bookmarklet that injects an extra script (I'm really excited to try this out) - A separate `script.js` file that's loaded into Datasette using the `"extra_js_urls"` metadata option, documented here: https://docs.datasette.io/en/stable/custom_templates.html#custom-css-and-javascript - A plugin you can install, like `datasette-vega` or `datasette-cluster-map` - since plugins can bundle their own script files that then get loaded on pages via this hook: https://docs.datasette.io/en/stable/plugin_hooks.html#extra-js-urls-template-database-table-columns-view-name-request-datasette | { "total_count": 1, "+1": 0, "-1": 0, "laugh": 0, "hooray": 1, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753217917 | https://api.github.com/repos/simonw/datasette/issues/983 | 753217917 | MDEyOklzc3VlQ29tbWVudDc1MzIxNzkxNw== | 9599 | 2020-12-31T22:23:29Z | 2020-12-31T22:23:36Z | OWNER | If I'm going to do that, it would be good if subsequent plugins that register against the `load` event are executed straight away. That's a bit of a weird edge-case in plugin world - it would involve the bulkier code that gets loaded redefining how `datasette.plugins.register` works to special-case the `'load'` hook. Maybe the tiny bootstrap code could define a `datasette.plugins.onload(callbackFunction)` method which gets upgraded later into something that fires straight away? Would add more bytes though. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753217714 | https://api.github.com/repos/simonw/datasette/issues/983 | 753217714 | MDEyOklzc3VlQ29tbWVudDc1MzIxNzcxNA== | 9599 | 2020-12-31T22:21:33Z | 2020-12-31T22:21:33Z | OWNER | Eventually I'd like to provide a whole bunch of other `datasette.X` utility functions that plugins can use - things like `datasette.addTabbedContentPane()` or similar. But I don't want to inline those into the page. So... I think the basic plugin system remains inline - maybe from an inlined file called `plugins-bootstrap.js`. Then a separate `plugins.js` contains the rest of the API functionality. If a plugin wants to take advantage of those APIs, maybe it registers itself using `datasette.plugins.register('load', () => ...)` - that `load` hook can then be fired once the bulkier plugin code has been loaded. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/987#issuecomment-753217127 | https://api.github.com/repos/simonw/datasette/issues/987 | 753217127 | MDEyOklzc3VlQ29tbWVudDc1MzIxNzEyNw== | 9599 | 2020-12-31T22:16:46Z | 2020-12-31T22:16:46Z | OWNER | I'm going to use `class="plugin-content-pre-table"` rather than `id=` - just because I still want to be able to display all of this stuff on the single https://latest.datasette.io/-/patterns page so duplicate IDs are best avoided. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712984738 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753215761 | https://api.github.com/repos/simonw/datasette/issues/983 | 753215761 | MDEyOklzc3VlQ29tbWVudDc1MzIxNTc2MQ== | 9599 | 2020-12-31T22:07:31Z | 2020-12-31T22:07:31Z | OWNER | I think I need to keep the mechanism whereby a plugin can return `undefined` in order to indicate that it has nothing to say for that specific item - that's borrowed from Pluggy and I've used it a bunch in my Python plugins. That makes the code a bit longer. I'll write some example plugins to help me decide if the filtering-out-of-undefined mechanism is needed or not. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-753215545 | https://api.github.com/repos/simonw/datasette/issues/983 | 753215545 | MDEyOklzc3VlQ29tbWVudDc1MzIxNTU0NQ== | 9599 | 2020-12-31T22:05:41Z | 2020-12-31T22:05:41Z | OWNER | Using object destructuring like that is a great idea. I'm going to play with your version - it's delightfully succinct. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753214664 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753214664 | MDEyOklzc3VlQ29tbWVudDc1MzIxNDY2NA== | 9599 | 2020-12-31T21:58:04Z | 2020-12-31T21:58:04Z | OWNER | Wrote a TIL about this: https://til.simonwillison.net/github-actions/prettier-github-actions | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753211535 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753211535 | MDEyOklzc3VlQ29tbWVudDc1MzIxMTUzNQ== | 9599 | 2020-12-31T21:46:04Z | 2020-12-31T21:46:04Z | OWNER | https://github.com/simonw/datasette/runs/1631682372?check_suite_focus=true failed! <img width="693" alt="Trying_out_bad_formatting__refs__1166_·_simonw_datasette_8087091" src="https://user-images.githubusercontent.com/9599/103426449-841e5c00-4b6e-11eb-95e6-26432a7b27dd.png"> | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753210536 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753210536 | MDEyOklzc3VlQ29tbWVudDc1MzIxMDUzNg== | 9599 | 2020-12-31T21:45:19Z | 2020-12-31T21:45:19Z | OWNER | Oops, committed that bad formatting test to `main` instead of a branch! | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753209192 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753209192 | MDEyOklzc3VlQ29tbWVudDc1MzIwOTE5Mg== | 9599 | 2020-12-31T21:44:22Z | 2020-12-31T21:44:22Z | OWNER | Tests passed in https://github.com/simonw/datasette/runs/1631677726?check_suite_focus=true I'm going to try submitting a pull request with badly formatted JavaScript to see if it gets caught. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753200580 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753200580 | MDEyOklzc3VlQ29tbWVudDc1MzIwMDU4MA== | 9599 | 2020-12-31T21:38:06Z | 2020-12-31T21:38:06Z | OWNER | I think this should work: ``` - uses: actions/cache@v2 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/prettier.yml' }} ``` I'll use the `prettier.yml` workflow that I'm about to create as the cache key for the NPM cache. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753197957 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753197957 | MDEyOklzc3VlQ29tbWVudDc1MzE5Nzk1Nw== | 9599 | 2020-12-31T21:36:14Z | 2020-12-31T21:36:14Z | OWNER | Maybe not that action actually - I wanted to use a pre-built action to avoid installing Prettier every time, but that's what it seems to do: https://github.com/creyD/prettier_action/blob/bb361e2979cff283ca7684908deac8f95400e779/entrypoint.sh#L28-L37 | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753195905 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753195905 | MDEyOklzc3VlQ29tbWVudDc1MzE5NTkwNQ== | 9599 | 2020-12-31T21:34:46Z | 2020-12-31T21:34:46Z | OWNER | This action looks good - tag 3.2 is equivalent to this commit hash: https://github.com/creyD/prettier_action/tree/bb361e2979cff283ca7684908deac8f95400e779 | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1166#issuecomment-753193475 | https://api.github.com/repos/simonw/datasette/issues/1166 | 753193475 | MDEyOklzc3VlQ29tbWVudDc1MzE5MzQ3NQ== | 9599 | 2020-12-31T21:33:00Z | 2020-12-31T21:33:00Z | OWNER | I want a CI check that confirms that files conform to prettier - but only `datasette/static/*.js` files that are not already minified. This seems to do the job: npx prettier --check 'datasette/static/*[!.min].js' | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
777140799 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752846267 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752846267 | MDEyOklzc3VlQ29tbWVudDc1Mjg0NjI2Nw== | 9599 | 2020-12-31T05:10:41Z | 2020-12-31T05:13:14Z | OWNER | https://github.com/PostHog/posthog/tree/master/cypress/integration has some useful examples, linked from this article: https://posthog.com/blog/cypress-end-to-end-tests Also useful: their workflow https://github.com/PostHog/posthog/blob/master/.github/workflows/e2e.yml | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752839433 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752839433 | MDEyOklzc3VlQ29tbWVudDc1MjgzOTQzMw== | 9599 | 2020-12-31T04:29:40Z | 2020-12-31T04:29:40Z | OWNER | Important to absorb the slightly bizarre assertion syntax from Chai - docs here https://www.chaijs.com/api/bdd/ | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752828851 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752828851 | MDEyOklzc3VlQ29tbWVudDc1MjgyODg1MQ== | 9599 | 2020-12-31T03:19:38Z | 2020-12-31T03:19:38Z | OWNER | I got Cypress working! I added the `datasette.plugins` code to the table template and ran a test called `plugins.spec.js` using the following: ```javascript context('datasette.plugins API', () => { beforeEach(() => { cy.visit('/fixtures/compound_three_primary_keys') }); it('should exist', () => { let datasette; cy.window().then(win => { datasette = win.datasette; }).then(() => { expect(datasette).to.exist; expect(datasette.plugins).to.exist; }); }); it('should register and execute plugins', () => { let datasette; cy.window().then(win => { datasette = win.datasette; }).then(() => { expect(datasette.plugins.call('numbers')).to.deep.equal([]); // Register a plugin datasette.plugins.register("numbers", (a, b) => a + b, ['a', 'b']); var result = datasette.plugins.call("numbers", {a: 1, b: 2}); expect(result).to.deep.equal([3]); // Second plugin datasette.plugins.register("numbers", (a, b) => a * b, ['a', 'b']); var result2 = datasette.plugins.call("numbers", {a: 1, b: 2}); expect(result2).to.deep.equal([3, 2]); }); }); }); ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752780000 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752780000 | MDEyOklzc3VlQ29tbWVudDc1Mjc4MDAwMA== | 9599 | 2020-12-30T22:41:25Z | 2020-12-30T22:41:25Z | OWNER | Jest works with Puppeteer: https://jestjs.io/docs/en/puppeteer | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752779820 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752779820 | MDEyOklzc3VlQ29tbWVudDc1Mjc3OTgyMA== | 9599 | 2020-12-30T22:40:28Z | 2020-12-30T22:40:28Z | OWNER | I don't know if Jest on the command-line is the right tool for this. It works for the `plugins.js` script but I'm increasingly going to want to start adding tests for browser JavaScript features - like the https://github.com/simonw/datasette/blob/0.53/datasette/static/table.js script - which will need to run in a browser. So maybe I should just find a browser testing solution and figure out how to run that under CI in GitHub Actions. Maybe https://www.cypress.io/ ? | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752779490 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752779490 | MDEyOklzc3VlQ29tbWVudDc1Mjc3OTQ5MA== | 9599 | 2020-12-30T22:38:43Z | 2020-12-30T22:38:43Z | OWNER | Turned that into a TIL: https://til.simonwillison.net/javascript/jest-without-package-json | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752777744 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752777744 | MDEyOklzc3VlQ29tbWVudDc1Mjc3Nzc0NA== | 9599 | 2020-12-30T22:30:24Z | 2020-12-30T22:30:24Z | OWNER | https://www.valentinog.com/blog/jest/ was useful. I created a `static/__tests__` folder and added this file as `plugins.spec.js`: ```javascript const datasette = require("../plugins.js"); describe("Datasette Plugins", () => { test("it should have datasette.plugins", () => { expect(!!datasette.plugins).toEqual(true); }); test("registering a plugin should work", () => { datasette.plugins.register("numbers", (a, b) => a + b, ["a", "b"]); var result = datasette.plugins.call("numbers", { a: 1, b: 2 }); expect(result).toEqual([3]); datasette.plugins.register("numbers", (a, b) => a * b, ["a", "b"]); var result2 = datasette.plugins.call("numbers", { a: 1, b: 2 }); expect(result2).toEqual([3, 2]); }); }); ``` In `static/plugins.js` I put this: ```javascript var datasette = datasette || {}; datasette.plugins = (() => { var registry = {}; return { register: (hook, fn, parameters) => { if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); }, call: (hook, args) => { args = args || {}; var results = []; (registry[hook] || []).forEach(([fn, parameters]) => { /* Call with the correct arguments */ var result = fn.apply(fn, parameters.map(parameter => args[parameter])); if (result !== undefined) { results.push(result); } }); return results; } }; })(); module.exports = datasette; ``` Note the `module.exports` line at the end. Then inside `static/` I ran the following command: ``` % npx jest -c '{}' PASS __tests__/plugins.spec.js Datasette Plugins ✓ it should have datasette.plugins (3 ms) ✓ registering a plugin should work (1 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 1.163 s Ran all test su… | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752773508 | https://api.github.com/repos/simonw/datasette/issues/983 | 752773508 | MDEyOklzc3VlQ29tbWVudDc1Mjc3MzUwOA== | 9599 | 2020-12-30T22:10:08Z | 2020-12-30T22:11:34Z | OWNER | https://twitter.com/dracos/status/1344402639476424706 points out that plugins returning 0 will be ignored. This should probably check for `result !== undefined` instead - knocks the size up to 250. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752770488 | https://api.github.com/repos/simonw/datasette/issues/983 | 752770488 | MDEyOklzc3VlQ29tbWVudDc1Mjc3MDQ4OA== | 9599 | 2020-12-30T21:55:35Z | 2020-12-30T21:58:26Z | OWNER | This one minifies to 241: ```javascript var datasette = datasette || {}; datasette.plugins = (() => { var registry = {}; return { register: (hook, fn, parameters) => { if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); }, call: (hook, args) => { args = args || {}; var results = []; (registry[hook] || []).forEach(([fn, parameters]) => { /* Call with the correct arguments */ var result = fn.apply(fn, parameters.map(parameter => args[parameter])); if (result) { results.push(result); } }); return results; } }; })(); ``` `var datasette=datasette||{};datasette.plugins=(()=>{var a={};return{register:(t,r,e)=>{a[t]||(a[t]=[]),a[t].push([r,e])},call:(t,r)=>{r=r||{};var e=[];return(a[t]||[]).forEach(([a,t])=>{var s=a.apply(a,t.map(a=>r[a]));s&&e.push(s)}),e}}})();` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752770133 | https://api.github.com/repos/simonw/datasette/issues/983 | 752770133 | MDEyOklzc3VlQ29tbWVudDc1Mjc3MDEzMw== | 9599 | 2020-12-30T21:53:45Z | 2020-12-30T21:54:22Z | OWNER | FixMyStreet inlines some JavaScript, and it's always a good idea to copy what they're doing when it comes to web performance: https://github.com/mysociety/fixmystreet/blob/23e9564b58a86b783ce47f3c0bf837cbd4fe7282/templates/web/base/common_header_tags.html#L19-L25 Note `var fixmystreet=fixmystreet||{};` which is shorter - https://twitter.com/dracos/status/1344399909794045954 | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-752769452 | https://api.github.com/repos/simonw/datasette/issues/1164 | 752769452 | MDEyOklzc3VlQ29tbWVudDc1Mjc2OTQ1Mg== | 9599 | 2020-12-30T21:50:16Z | 2020-12-30T21:50:16Z | OWNER | If I implement this I can automate the CodeMirror minification and remove the bit about running `uglify-js` against it from the documentation here: https://docs.datasette.io/en/0.53/contributing.html#upgrading-codemirror | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-752768785 | https://api.github.com/repos/simonw/datasette/issues/1164 | 752768785 | MDEyOklzc3VlQ29tbWVudDc1Mjc2ODc4NQ== | 9599 | 2020-12-30T21:47:06Z | 2020-12-30T21:47:06Z | OWNER | If I'm going to minify `table.js` I'd like to offer a source map for it. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-752768652 | https://api.github.com/repos/simonw/datasette/issues/1164 | 752768652 | MDEyOklzc3VlQ29tbWVudDc1Mjc2ODY1Mg== | 9599 | 2020-12-30T21:46:29Z | 2020-12-30T21:46:29Z | OWNER | Running https://skalman.github.io/UglifyJS-online/ against https://github.com/simonw/datasette/blob/0.53/datasette/static/table.js knocks it down from 7810 characters to 4643. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752767500 | https://api.github.com/repos/simonw/datasette/issues/983 | 752767500 | MDEyOklzc3VlQ29tbWVudDc1Mjc2NzUwMA== | 9599 | 2020-12-30T21:42:07Z | 2020-12-30T21:42:07Z | OWNER | Another option: have both "dev" and "production" versions of the plugin mechanism script. Make it easy to switch between the two. Build JavaScript unit tests that exercise the "production" APIs against the development version, and have extra tests that just work against the features in the development version. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752767174 | https://api.github.com/repos/simonw/datasette/issues/983 | 752767174 | MDEyOklzc3VlQ29tbWVudDc1Mjc2NzE3NA== | 9599 | 2020-12-30T21:40:44Z | 2020-12-30T21:40:44Z | OWNER | Started a Twitter thread about this here: https://twitter.com/simonw/status/1344392603794477056 | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752751490 | https://api.github.com/repos/simonw/datasette/issues/983 | 752751490 | MDEyOklzc3VlQ29tbWVudDc1Mjc1MTQ5MA== | 9599 | 2020-12-30T20:40:04Z | 2020-12-30T21:34:22Z | OWNER | This one is 683 bytes with Uglify - I like how https://skalman.github.io/UglifyJS-online/ shows you the minified character count as you edit the script: ```javascript window.datasette = window.datasette || {}; window.datasette.plugins = (() => { var registry = {}; var definitions = {}; var stringify = JSON.stringify; function extractParameters(fn) { var match = /\((.*)\)/.exec(fn.toString()); if (match && match[1].trim()) { return match[1].split(',').map(s => s.trim()); } else { return []; } } function isSubSet(a, b) { return a.every(parameter => b.includes(parameter)) } return { _r: registry, define: (hook, parameters) => { definitions[hook] = parameters || []; }, register: (hook, fn, parameters) => { parameters = parameters || extractParameters(fn); if (!definitions[hook]) { throw 'Hook "' + hook + '" not defined'; } /* Check parameters is a subset of definitions[hook] */ var validParameters = definitions[hook]; if (!isSubSet(parameters, validParameters)) { throw '"' + hook + '" valid args: ' + stringify(validParameters); } if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); }, call: (hook, args) => { args = args || {}; if (!definitions[hook]) { throw '"' + hook + '" hook not defined'; } if (!isSubSet(Object.keys(args), definitions[hook])) { throw '"' + hook + '" valid args: ' + stringify(definitions[hook]); } var implementations = registry[hook] || []; var results = []; implementations.forEach(([fn, parameters]) => { /* Call with the correct… | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752760815 | https://api.github.com/repos/simonw/datasette/issues/983 | 752760815 | MDEyOklzc3VlQ29tbWVudDc1Mjc2MDgxNQ== | 9599 | 2020-12-30T21:15:41Z | 2020-12-30T21:15:41Z | OWNER | I'm going to write a few example plugins and try them out against the longer and shorter versions of the script, to get a better feel for how useful the longer versions with the error handling and explicit definition actually are. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752760054 | https://api.github.com/repos/simonw/datasette/issues/983 | 752760054 | MDEyOklzc3VlQ29tbWVudDc1Mjc2MDA1NA== | 9599 | 2020-12-30T21:12:36Z | 2020-12-30T21:14:05Z | OWNER | I gotta admit that 262 byte version is pretty tempting, if it's going to end up in the `<head>` of every single page. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752759885 | https://api.github.com/repos/simonw/datasette/issues/983 | 752759885 | MDEyOklzc3VlQ29tbWVudDc1Mjc1OTg4NQ== | 9599 | 2020-12-30T21:11:52Z | 2020-12-30T21:14:00Z | OWNER | 262 bytes if I remove the parameter introspection code, instead requiring plugin authors to specify the arguments they take: ```javascript window.datasette = window.datasette || {}; window.datasette.plugins = (() => { var registry = {}; return { register: (hook, fn, parameters) => { if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); }, call: (hook, args) => { args = args || {}; var results = []; (registry[hook] || []).forEach(([fn, parameters]) => { /* Call with the correct arguments */ var callWith = parameters.map(parameter => args[parameter]); var result = fn.apply(fn, callWith); if (result) { results.push(result); } }); return results; } }; })(); ``` `window.datasette=window.datasette||{},window.datasette.plugins=(()=>{var a={};return{register:(t,e,r)=>{a[t]||(a[t]=[]),a[t].push([e,r])},call:(t,e)=>{e=e||{};var r=[];return(a[t]||[]).forEach(([a,t])=>{var s=t.map(a=>e[a]),d=a.apply(a,s);d&&r.push(d)}),r}}})();` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752758802 | https://api.github.com/repos/simonw/datasette/issues/983 | 752758802 | MDEyOklzc3VlQ29tbWVudDc1Mjc1ODgwMg== | 9599 | 2020-12-30T21:07:33Z | 2020-12-30T21:10:10Z | OWNER | Removing the `datasette.plugin.define()` method and associated error handling reduces the uglified version from 683 bytes to 380 bytes. I think the error checking is worth the extra 303 bytes per page load, even if it's only really needed for a better developer experience. ```javascript window.datasette = window.datasette || {}; window.datasette.plugins = (() => { var registry = {}; function extractParameters(fn) { var match = /\((.*)\)/.exec(fn.toString()); if (match && match[1].trim()) { return match[1].split(',').map(s => s.trim()); } else { return []; } } return { register: (hook, fn, parameters) => { parameters = parameters || extractParameters(fn); if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); }, call: (hook, args) => { args = args || {}; var implementations = registry[hook] || []; var results = []; implementations.forEach(([fn, parameters]) => { /* Call with the correct arguments */ var callWith = parameters.map(parameter => args[parameter]); var result = fn.apply(fn, callWith); if (result) { results.push(result); } }); return results; } }; })(); ``` `window.datasette=window.datasette||{},window.datasette.plugins=(()=>{var t={};return{register:(r,a,e)=>{e=e||function(t){var r=/\((.*)\)/.exec(t.toString());return r&&r[1].trim()?r[1].split(",").map(t=>t.trim()):[]}(a),t[r]||(t[r]=[]),t[r].push([a,e])},call:(r,a)=>{a=a||{};var e=t[r]||[],i=[];return e.forEach(([t,r])=>{var e=r.map(t=>a[t]),n=t.apply(t,e);n&&i.push(n)}),i}}})();` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/1165#issuecomment-752757910 | https://api.github.com/repos/simonw/datasette/issues/1165 | 752757910 | MDEyOklzc3VlQ29tbWVudDc1Mjc1NzkxMA== | 9599 | 2020-12-30T21:04:18Z | 2020-12-30T21:04:18Z | OWNER | https://jestjs.io/ looks worth trying here. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776635426 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752757289 | https://api.github.com/repos/simonw/datasette/issues/983 | 752757289 | MDEyOklzc3VlQ29tbWVudDc1Mjc1NzI4OQ== | 9599 | 2020-12-30T21:02:20Z | 2020-12-30T21:02:20Z | OWNER | I'm going to need to add JavaScript unit tests for this new plugin system. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-752757075 | https://api.github.com/repos/simonw/datasette/issues/1164 | 752757075 | MDEyOklzc3VlQ29tbWVudDc1Mjc1NzA3NQ== | 9599 | 2020-12-30T21:01:27Z | 2020-12-30T21:01:27Z | OWNER | I don't want Datasette contributors to need a working Node.js install to run the tests or work on Datasette unless they are explicitly working on the JavaScript. I think I'm going to do this with a unit test that runs only if `upglify-js` is available on the path and confirms that the `*.min.js` version of each script in the repository correctly matches the results from running `uglify-js` against it. That way if anyone checks in a change to JavaScript but forgets to run the minifier the tests will fail in CI. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/1164#issuecomment-752756612 | https://api.github.com/repos/simonw/datasette/issues/1164 | 752756612 | MDEyOklzc3VlQ29tbWVudDc1Mjc1NjYxMg== | 9599 | 2020-12-30T20:59:54Z | 2020-12-30T20:59:54Z | OWNER | I tried a few different pure-Python JavaScript minifying libraries and none of them produced results as good as https://www.npmjs.com/package/uglify-js for the plugin code I'm considering in #983. So I think I'll need to rely on a Node.js tool for this. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
776634318 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752750551 | https://api.github.com/repos/simonw/datasette/issues/983 | 752750551 | MDEyOklzc3VlQ29tbWVudDc1Mjc1MDU1MQ== | 9599 | 2020-12-30T20:36:38Z | 2020-12-30T20:37:48Z | OWNER | This version minifies to 702 characters: ```javascript window.datasette = window.datasette || {}; window.datasette.plugins = (() => { var registry = {}; var definitions = {}; var stringify = JSON.stringify; function extractParameters(fn) { var match = /\((.*)\)/.exec(fn.toString()); if (match && match[1].trim()) { return match[1].split(',').map(s => s.trim()); } else { return []; } } function isSubSet(a, b) { return a.every(parameter => b.includes(parameter)) } return { _registry: registry, define: (hook, parameters) => { definitions[hook] = parameters || []; }, register: (hook, fn, parameters) => { parameters = parameters || extractParameters(fn); if (!definitions[hook]) { throw '"' + hook + '" is not a defined hook'; } /* Check parameters is a subset of definitions[hook] */ var validParameters = definitions[hook]; if (!isSubSet(parameters, validParameters)) { throw '"' + hook + '" valid args are ' + stringify(validParameters); } if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); }, call: (hook, args) => { args = args || {}; if (!definitions[hook]) { throw '"' + hook + '" hook is not defined'; } if (!isSubSet(Object.keys(args), definitions[hook])) { throw '"' + hook + '" valid args: ' + stringify(definitions[hook]); } var implementations = registry[hook] || []; var results = []; implementations.forEach(([fn, parameters]) => { /* Call with the correct arguments */ var callWith = parameters.map(parameter => args[parameter]); … | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752749189 | https://api.github.com/repos/simonw/datasette/issues/983 | 752749189 | MDEyOklzc3VlQ29tbWVudDc1Mjc0OTE4OQ== | 9599 | 2020-12-30T20:31:28Z | 2020-12-30T20:31:28Z | OWNER | Using raw string exceptions, `throw '"' + hook + '" hook has not been defined';`, knocks it down to 795 characters. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752748496 | https://api.github.com/repos/simonw/datasette/issues/983 | 752748496 | MDEyOklzc3VlQ29tbWVudDc1Mjc0ODQ5Ng== | 9599 | 2020-12-30T20:28:48Z | 2020-12-30T20:28:48Z | OWNER | If I'm going to minify it I'll need to figure out a build step in Datasette itself so that I can easily work on that minified version. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752747999 | https://api.github.com/repos/simonw/datasette/issues/983 | 752747999 | MDEyOklzc3VlQ29tbWVudDc1Mjc0Nzk5OQ== | 9599 | 2020-12-30T20:27:00Z | 2020-12-30T20:27:00Z | OWNER | I need to decide how this code is going to be loaded. Putting it in a blocking `<script>` element in the head would work, but I'd rather not block loading of the rest of the page. Using a `<script async>` method would be nicer, but then I have to worry about plugins attempting to register themselves before the page has fully loaded. Running it through https://javascript-minifier.com/ produces this, which is 855 characters - so maybe I could inline that into the header of the page? `window.datasette={},window.datasette.plugins=function(){var r={},n={};function e(r,n){return r.every(r=>n.includes(r))}return{define:function(r,e){n[r]=e||[]},register:function(t,i,o){if(o=o||function(r){var n=/\((.*)\)/.exec(r.toString());return n&&n[1].trim()?n[1].split(",").map(r=>r.trim()):[]}(i),!n[t])throw new Error('"'+t+'" is not a defined plugin hook');if(!n[t])throw new Error('"'+t+'" is not a defined plugin hook');var a=n[t];if(!e(o,a))throw new Error('"'+t+'" valid parameters are '+JSON.stringify(a));r[t]||(r[t]=[]),r[t].push([i,o])},_registry:r,call:function(t,i){if(i=i||{},!n[t])throw new Error('"'+t+'" hook has not been defined');if(!e(Object.keys(i),n[t]))throw new Error('"'+t+'" valid arguments are '+JSON.stringify(n[t]));var o=r[t]||[],a=[];return o.forEach(([r,n])=>{var e=n.map(r=>i[r]),t=r.apply(r,e);t&&a.push(t)}),a}}}();` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752747169 | https://api.github.com/repos/simonw/datasette/issues/983 | 752747169 | MDEyOklzc3VlQ29tbWVudDc1Mjc0NzE2OQ== | 9599 | 2020-12-30T20:24:07Z | 2020-12-30T20:24:07Z | OWNER | This version adds `datasette.plugins.define()` plus extra validation of both `.register()` and `.call()`: ```javascript window.datasette = {}; window.datasette.plugins = (function() { var registry = {}; var definitions = {}; function extractParameters(fn) { var match = /\((.*)\)/.exec(fn.toString()); if (match && match[1].trim()) { return match[1].split(',').map(s => s.trim()); } else { return []; } } function define(hook, parameters) { definitions[hook] = parameters || []; } function isSubSet(a, b) { return a.every(parameter => b.includes(parameter)) } function register(hook, fn, parameters) { parameters = parameters || extractParameters(fn); if (!definitions[hook]) { throw new Error('"' + hook + '" is not a defined plugin hook'); } if (!definitions[hook]) { throw new Error('"' + hook + '" is not a defined plugin hook'); } /* Check parameters is a subset of definitions[hook] */ var validParameters = definitions[hook]; if (!isSubSet(parameters, validParameters)) { throw new Error('"' + hook + '" valid parameters are ' + JSON.stringify(validParameters)); } if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); } function call(hook, args) { args = args || {}; if (!definitions[hook]) { throw new Error('"' + hook + '" hook has not been defined'); } if (!isSubSet(Object.keys(args), definitions[hook])) { throw new Error('"' + hook + '" valid arguments are ' + JSON.stringify(definitions[hook])); } var implementations = registry[hook] || []; var results = []; implementations.forEach(([fn, parameters]) => { /* Call with the correct arguments */ var callWith =… | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752744311 | https://api.github.com/repos/simonw/datasette/issues/983 | 752744311 | MDEyOklzc3VlQ29tbWVudDc1Mjc0NDMxMQ== | 9599 | 2020-12-30T20:12:50Z | 2020-12-30T20:13:02Z | OWNER | This could work to define a plugin hook: ```javascript datasette.plugins.define('numbers', ['a' ,'b']) ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752744195 | https://api.github.com/repos/simonw/datasette/issues/983 | 752744195 | MDEyOklzc3VlQ29tbWVudDc1Mjc0NDE5NQ== | 9599 | 2020-12-30T20:12:26Z | 2020-12-30T20:12:26Z | OWNER | This implementation doesn't have an equivalent of "hookspecs" which can identify if a registered plugin implementation matches a known signature. I should add that, it will provide a better developer experience if someone has a typo. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752742669 | https://api.github.com/repos/simonw/datasette/issues/983 | 752742669 | MDEyOklzc3VlQ29tbWVudDc1Mjc0MjY2OQ== | 9599 | 2020-12-30T20:07:05Z | 2020-12-30T20:07:18Z | OWNER | Initial prototype: ```javascript window.datasette = {}; window.datasette.plugins = (function() { var registry = {}; function extractParameters(fn) { var match = /\((.*)\)/.exec(fn.toString()); if (match && match[1].trim()) { return match[1].split(',').map(s => s.trim()); } else { return []; } } function register(hook, fn, parameters) { parameters = parameters || extractParameters(fn); if (!registry[hook]) { registry[hook] = []; } registry[hook].push([fn, parameters]); } function call(hook, args) { args = args || {}; var implementations = registry[hook] || []; var results = []; implementations.forEach(([fn, parameters]) => { /* Call with the correct arguments */ var callWith = parameters.map(parameter => args[parameter]); var result = fn.apply(fn, callWith); if (result) { results.push(result); } }); return results; } return { register: register, _registry: registry, call: call }; })(); ``` Usage example: ```javascript datasette.plugins.register('numbers', (a, b) => a + b) datasette.plugins.register('numbers', (a, b) => a * b) datasette.plugins.call('numbers', {a: 4, b: 6}) /* Returns [10, 24] */ ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752729035 | https://api.github.com/repos/simonw/datasette/issues/983 | 752729035 | MDEyOklzc3VlQ29tbWVudDc1MjcyOTAzNQ== | 9599 | 2020-12-30T19:15:56Z | 2020-12-30T19:16:44Z | OWNER | The `column_actions` hook is the obvious first place to try this out. What are some demo plugins I could build for it? - Word cloud for this column - Count values (essentially linking to the SQL query for that column, as an extended version of the facet counts) - would be great if this could include pagination somehow, via #856. - Extract this column into a separate table - Add an index to this column | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752722863 | https://api.github.com/repos/simonw/datasette/issues/983 | 752722863 | MDEyOklzc3VlQ29tbWVudDc1MjcyMjg2Mw== | 9599 | 2020-12-30T18:52:39Z | 2020-12-30T18:52:39Z | OWNER | Then to call the plugins: ```javascript datasette.plugins.call('column_actions', {database: 'database', table: 'table'}) ``` | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752721840 | https://api.github.com/repos/simonw/datasette/issues/983 | 752721840 | MDEyOklzc3VlQ29tbWVudDc1MjcyMTg0MA== | 9599 | 2020-12-30T18:48:53Z | 2020-12-30T18:51:51Z | OWNER | Potential design: ```javascript datasette.plugins.register('column_actions', function(database, table, column, actor) { /* ... *l }) ``` Or if you want to be explicit to survive minification: ```javascript datasette.plugins.register('column_actions', function(database, table, column, actor) { /* ... *l }, ['database', 'table', 'column', 'actor']) ``` I'm making that list of parameter names an optional third argument to the `register()` function. If that argument isn't passed, introspection will be used to figure out the parameter names. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752721069 | https://api.github.com/repos/simonw/datasette/issues/983 | 752721069 | MDEyOklzc3VlQ29tbWVudDc1MjcyMTA2OQ== | 9599 | 2020-12-30T18:46:10Z | 2020-12-30T18:46:10Z | OWNER | Pluggy does dependency injection by introspecting the named arguments to the Python function, which I really like. That's tricker in JavaScript. It looks like the only way to introspect a function is to look at the `.toString()` representation of it and parse the `(parameter, list)` using a regular expression. Even more challenging: JavaScript developers love minifying their code, and minification can shorten the function parameter names. From https://code-maven.com/dependency-injection-in-angularjs it looks like Angular.js does dependency injection and solves this by letting you optionally provide a separate list of the arguments your function uses: ```javascript angular.module('DemoApp', []) .controller('DemoController', ['$scope', '$log', function($scope, $log) { $scope.message = "Hello World"; $log.debug('logging hello'); }]); ``` I can copy that approach: I'll introspect by default, but provide a documented mechanism for explicitly listing your parameter names so that if you know your plugin code will be minified you can use that instead. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 | |
https://github.com/simonw/datasette/issues/983#issuecomment-752715412 | https://api.github.com/repos/simonw/datasette/issues/983 | 752715412 | MDEyOklzc3VlQ29tbWVudDc1MjcxNTQxMg== | 9599 | 2020-12-30T18:25:31Z | 2020-12-30T18:25:31Z | OWNER | I'm going to introduce a global `datasette` object which holds all the documented JavaScript API for plugin authors. | { "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } |
712260429 |